From 18690e92e2b82094353b4a6095857574e87d905e Mon Sep 17 00:00:00 2001 From: mroccyen Date: Mon, 7 Mar 2022 15:50:46 +0800 Subject: [PATCH 01/29] Optimize StreamDecoder --- .../benchmark/DecoderIteratorsBenchmark.java | 2 +- .../main/java/feign/stream/StreamDecoder.java | 42 ++++++++++++++----- .../java/feign/stream/StreamDecoderTest.java | 21 ++++------ 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java index 36cf0c665..a27bf3bcc 100644 --- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java +++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java @@ -99,7 +99,7 @@ public void buildDecoder() { type = new TypeReference>() {}.getType(); break; case "stream": - decoder = StreamDecoder.create(JacksonIteratorDecoder.create()); + decoder = StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()); type = new TypeReference>() {}.getType(); break; default: diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java index c5e796395..a9c47415d 100644 --- a/core/src/main/java/feign/stream/StreamDecoder.java +++ b/core/src/main/java/feign/stream/StreamDecoder.java @@ -16,6 +16,7 @@ import feign.FeignException; import feign.Response; import feign.codec.Decoder; +import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.lang.reflect.ParameterizedType; @@ -24,6 +25,7 @@ import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import static feign.Util.UTF_8; import static feign.Util.ensureClosed; /** @@ -35,7 +37,7 @@ *
  * 
  * Feign.builder()
- *   .decoder(StreamDecoder.create(JacksonIteratorDecoder.create()))
+ *   .decoder(StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()))
  *   .doNotCloseAfterDecode() // Required for streaming
  *   .target(GitHub.class, "https://api.github.com");
  * interface GitHub {
@@ -43,27 +45,28 @@
  *   Stream contributors(@Param("owner") String owner, @Param("repo") String repo);
  * }
  * 
+ * + * @author mroccyen */ public final class StreamDecoder implements Decoder { + private final Decoder delegate; private final Decoder iteratorDecoder; - StreamDecoder(Decoder iteratorDecoder) { + StreamDecoder(Decoder delegate, Decoder iteratorDecoder) { + this.delegate = delegate; this.iteratorDecoder = iteratorDecoder; } @Override public Object decode(Response response, Type type) throws IOException, FeignException { - if (!(type instanceof ParameterizedType)) { - throw new IllegalArgumentException("StreamDecoder supports only stream: unknown " + type); + if (!isStream(type)) { + return delegate.decode(response, type); } ParameterizedType streamType = (ParameterizedType) type; - if (!Stream.class.equals(streamType.getRawType())) { - throw new IllegalArgumentException("StreamDecoder supports only stream: unknown " + type); - } - Iterator iterator = - (Iterator) iteratorDecoder.decode(response, new IteratorParameterizedType(streamType)); + Iterator iterator = (Iterator) iteratorDecoder.decode( + response, new IteratorParameterizedType(streamType)); return StreamSupport.stream( Spliterators.spliteratorUnknownSize(iterator, 0), false) @@ -76,8 +79,16 @@ public Object decode(Response response, Type type) }); } - public static StreamDecoder create(Decoder iteratorDecoder) { - return new StreamDecoder(iteratorDecoder); + private boolean isStream(Type type) { + if (!(type instanceof ParameterizedType)) { + return false; + } + ParameterizedType parameterizedType = (ParameterizedType) type; + return parameterizedType.getRawType().equals(Stream.class); + } + + public static StreamDecoder create(Decoder delegate, Decoder iteratorDecoder) { + return new StreamDecoder(delegate, iteratorDecoder); } static final class IteratorParameterizedType implements ParameterizedType { @@ -103,4 +114,13 @@ public Type getOwnerType() { return null; } } + + public static class DefaultIteratorDecoder implements Decoder { + @Override + public Object decode(Response response, Type type) throws IOException { + BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + } + } + } diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java index 0a9f94e62..13c8d965a 100644 --- a/core/src/test/java/feign/stream/StreamDecoderTest.java +++ b/core/src/test/java/feign/stream/StreamDecoderTest.java @@ -14,13 +14,12 @@ package feign.stream; import com.fasterxml.jackson.core.type.TypeReference; -import feign.Feign; -import feign.Request; +import feign.*; import feign.Request.HttpMethod; -import feign.RequestLine; -import feign.Response; -import feign.Util; -import java.io.BufferedReader; +import feign.codec.Decoder; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Test; import java.io.Closeable; import java.io.IOException; import java.util.Arrays; @@ -28,9 +27,6 @@ import java.util.Iterator; import java.util.stream.Collectors; import java.util.stream.Stream; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.Test; import static feign.Util.UTF_8; import static org.assertj.core.api.Assertions.assertThat; @@ -68,9 +64,8 @@ public void simpleStreamTest() { server.enqueue(new MockResponse().setBody("foo\nbar")); StreamInterface api = Feign.builder() - .decoder(StreamDecoder.create( - (response, type) -> new BufferedReader(response.body().asReader(UTF_8)).lines() - .iterator())) + .decoder(StreamDecoder.create(new Decoder.Default(), + new StreamDecoder.DefaultIteratorDecoder())) .doNotCloseAfterDecode() .target(StreamInterface.class, server.url("/").toString()); @@ -90,7 +85,7 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException { .build(); TestCloseableIterator it = new TestCloseableIterator(); - StreamDecoder decoder = new StreamDecoder((r, t) -> it); + StreamDecoder decoder = new StreamDecoder(new Decoder.Default(), (r, t) -> it); try (Stream stream = (Stream) decoder.decode(response, new TypeReference>() {}.getType())) { From 149accdf0eac38c89bc9f91afb58768e1f79f59a Mon Sep 17 00:00:00 2001 From: mroccyen Date: Tue, 8 Mar 2022 11:40:59 +0800 Subject: [PATCH 02/29] Optimize StreamDecoder --- .../benchmark/DecoderIteratorsBenchmark.java | 2 +- .../java/feign/stream/IteratorDecoder.java | 52 +++++++++++++++++++ .../main/java/feign/stream/StreamDecoder.java | 42 +++++++++------ .../java/feign/stream/StreamDecoderTest.java | 23 ++++++-- 4 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 core/src/main/java/feign/stream/IteratorDecoder.java diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java index a27bf3bcc..13415463b 100644 --- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java +++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java @@ -99,7 +99,7 @@ public void buildDecoder() { type = new TypeReference>() {}.getType(); break; case "stream": - decoder = StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()); + decoder = StreamDecoder.create(new Decoder.Default()); type = new TypeReference>() {}.getType(); break; default: diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java new file mode 100644 index 000000000..18a757c94 --- /dev/null +++ b/core/src/main/java/feign/stream/IteratorDecoder.java @@ -0,0 +1,52 @@ +package feign.stream; + +import feign.FeignException; +import feign.Response; +import feign.codec.DecodeException; +import feign.codec.Decoder; + +import java.io.BufferedReader; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Iterator; + +import static feign.Util.UTF_8; + +/** + * @author mroccyen + */ +public interface IteratorDecoder extends Decoder { + /** + * Decodes an http response into an iterator. If you need to + * wrap exceptions, please do so via {@link DecodeException}. + * + * @param response the response to decode + * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the + * method corresponding to this {@code response}. + * @return instance of {@code type} + * @throws IOException will be propagated safely to the caller. + * @throws DecodeException when decoding failed due to a checked exception besides IOException. + * @throws FeignException when decoding succeeds, but conveys the operation failed. + */ + Iterator decodeIterator(Response response, Type type) throws IOException, DecodeException, FeignException; + + class LineToIteratorDecoder implements IteratorDecoder { + + private final Decoder delegate; + + public LineToIteratorDecoder(Decoder delegate) { + this.delegate = delegate; + } + + @Override + public Object decode(Response response, Type type) throws IOException { + return delegate.decode(response, type); + } + + @Override + public Iterator decodeIterator(Response response, Type type) throws IOException, DecodeException, FeignException { + BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + } + } +} diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java index a9c47415d..a3edc9800 100644 --- a/core/src/main/java/feign/stream/StreamDecoder.java +++ b/core/src/main/java/feign/stream/StreamDecoder.java @@ -16,7 +16,7 @@ import feign.FeignException; import feign.Response; import feign.codec.Decoder; -import java.io.BufferedReader; + import java.io.Closeable; import java.io.IOException; import java.lang.reflect.ParameterizedType; @@ -25,7 +25,7 @@ import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static feign.Util.UTF_8; + import static feign.Util.ensureClosed; /** @@ -37,7 +37,7 @@ *
  * 
  * Feign.builder()
- *   .decoder(StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()))
+ *   .decoder(StreamDecoder.create(new Decoder.Default()))
  *   .doNotCloseAfterDecode() // Required for streaming
  *   .target(GitHub.class, "https://api.github.com");
  * interface GitHub {
@@ -51,11 +51,10 @@
 public final class StreamDecoder implements Decoder {
 
   private final Decoder delegate;
-  private final Decoder iteratorDecoder;
+  private IteratorDecoder iteratorDecoder;
 
-  StreamDecoder(Decoder delegate, Decoder iteratorDecoder) {
+  public StreamDecoder(Decoder delegate) {
     this.delegate = delegate;
-    this.iteratorDecoder = iteratorDecoder;
   }
 
   @Override
@@ -64,8 +63,14 @@ public Object decode(Response response, Type type)
     if (!isStream(type)) {
       return delegate.decode(response, type);
     }
+    IteratorDecoder iteratorDecoder;
+    if (delegate instanceof IteratorDecoder) {
+      iteratorDecoder = (IteratorDecoder) delegate;
+    } else {
+      iteratorDecoder = getIteratorDecoder(delegate);
+    }
     ParameterizedType streamType = (ParameterizedType) type;
-    Iterator iterator = (Iterator) iteratorDecoder.decode(
+    Iterator iterator = iteratorDecoder.decodeIterator(
         response, new IteratorParameterizedType(streamType));
 
     return StreamSupport.stream(
@@ -87,8 +92,19 @@ private boolean isStream(Type type) {
     return parameterizedType.getRawType().equals(Stream.class);
   }
 
-  public static StreamDecoder create(Decoder delegate, Decoder iteratorDecoder) {
-    return new StreamDecoder(delegate, iteratorDecoder);
+  private IteratorDecoder getIteratorDecoder(Decoder delegate) {
+    synchronized (this) {
+      if (iteratorDecoder == null) {
+        synchronized (this) {
+          iteratorDecoder = new IteratorDecoder.LineToIteratorDecoder(delegate);
+        }
+      }
+    }
+    return iteratorDecoder;
+  }
+
+  public static StreamDecoder create(Decoder delegate) {
+    return new StreamDecoder(delegate);
   }
 
   static final class IteratorParameterizedType implements ParameterizedType {
@@ -115,12 +131,4 @@ public Type getOwnerType() {
     }
   }
 
-  public static class DefaultIteratorDecoder implements Decoder {
-    @Override
-    public Object decode(Response response, Type type) throws IOException {
-      BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8));
-      return bufferedReader.lines().iterator();
-    }
-  }
-
 }
diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java
index 13c8d965a..61bebebeb 100644
--- a/core/src/test/java/feign/stream/StreamDecoderTest.java
+++ b/core/src/test/java/feign/stream/StreamDecoderTest.java
@@ -16,12 +16,14 @@
 import com.fasterxml.jackson.core.type.TypeReference;
 import feign.*;
 import feign.Request.HttpMethod;
+import feign.codec.DecodeException;
 import feign.codec.Decoder;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.junit.Test;
 import java.io.Closeable;
 import java.io.IOException;
+import java.lang.reflect.Type;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
@@ -64,8 +66,7 @@ public void simpleStreamTest() {
     server.enqueue(new MockResponse().setBody("foo\nbar"));
 
     StreamInterface api = Feign.builder()
-        .decoder(StreamDecoder.create(new Decoder.Default(),
-            new StreamDecoder.DefaultIteratorDecoder()))
+        .decoder(StreamDecoder.create(new Decoder.Default()))
         .doNotCloseAfterDecode()
         .target(StreamInterface.class, server.url("/").toString());
 
@@ -85,10 +86,10 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
         .build();
 
     TestCloseableIterator it = new TestCloseableIterator();
-    StreamDecoder decoder = new StreamDecoder(new Decoder.Default(), (r, t) -> it);
+    StreamDecoder decoder = new StreamDecoder(new TestIteratorDecoder(it));
 
     try (Stream stream =
-        (Stream) decoder.decode(response, new TypeReference>() {}.getType())) {
+        (Stream) decoder.decode(response, new TypeReference>() {}.getType())) {
       assertThat(stream.collect(Collectors.toList())).hasSize(1);
       assertThat(it.called).isTrue();
     } finally {
@@ -96,6 +97,20 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
     }
   }
 
+  static class TestIteratorDecoder extends Decoder.Default implements IteratorDecoder {
+
+    final TestCloseableIterator testCloseableIterator;
+
+    TestIteratorDecoder(TestCloseableIterator testCloseableIterator) {
+      this.testCloseableIterator = testCloseableIterator;
+    }
+
+    @Override
+    public Iterator decodeIterator(Response response, Type type) throws FeignException {
+      return testCloseableIterator;
+    }
+  }
+
   static class TestCloseableIterator implements Iterator, Closeable {
     boolean called;
     boolean closed;

From 29e0fd5b0dd950976e02405890dc9e5fc9a479d9 Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Tue, 8 Mar 2022 11:49:30 +0800
Subject: [PATCH 03/29] add license header

---
 .../src/main/java/feign/stream/IteratorDecoder.java | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java
index 18a757c94..4ae114c9a 100644
--- a/core/src/main/java/feign/stream/IteratorDecoder.java
+++ b/core/src/main/java/feign/stream/IteratorDecoder.java
@@ -1,3 +1,16 @@
+/*
+ * Copyright 2012-2022 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.stream;
 
 import feign.FeignException;

From ee9a88dc25c7570983d8844fb57738593f2484cc Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Tue, 8 Mar 2022 11:54:50 +0800
Subject: [PATCH 04/29] add license header

---
 .../java/feign/stream/IteratorDecoder.java    | 20 +++++++++----------
 .../main/java/feign/stream/StreamDecoder.java |  2 --
 2 files changed, 10 insertions(+), 12 deletions(-)

diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java
index 4ae114c9a..d330c16cc 100644
--- a/core/src/main/java/feign/stream/IteratorDecoder.java
+++ b/core/src/main/java/feign/stream/IteratorDecoder.java
@@ -17,12 +17,10 @@
 import feign.Response;
 import feign.codec.DecodeException;
 import feign.codec.Decoder;
-
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.lang.reflect.Type;
 import java.util.Iterator;
-
 import static feign.Util.UTF_8;
 
 /**
@@ -30,18 +28,19 @@
  */
 public interface IteratorDecoder extends Decoder {
   /**
-   * Decodes an http response into an iterator. If you need to
-   * wrap exceptions, please do so via {@link DecodeException}.
+   * Decodes an http response into an iterator. If you need to wrap exceptions, please do so via
+   * {@link DecodeException}.
    *
    * @param response the response to decode
-   * @param type     {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the
-   *                 method corresponding to this {@code response}.
+   * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the
+   *        method corresponding to this {@code response}.
    * @return instance of {@code type}
-   * @throws IOException     will be propagated safely to the caller.
+   * @throws IOException will be propagated safely to the caller.
    * @throws DecodeException when decoding failed due to a checked exception besides IOException.
-   * @throws FeignException  when decoding succeeds, but conveys the operation failed.
+   * @throws FeignException when decoding succeeds, but conveys the operation failed.
    */
-  Iterator decodeIterator(Response response, Type type) throws IOException, DecodeException, FeignException;
+  Iterator decodeIterator(Response response, Type type)
+      throws IOException, DecodeException, FeignException;
 
   class LineToIteratorDecoder implements IteratorDecoder {
 
@@ -57,7 +56,8 @@ public Object decode(Response response, Type type) throws IOException {
     }
 
     @Override
-    public Iterator decodeIterator(Response response, Type type) throws IOException, DecodeException, FeignException {
+    public Iterator decodeIterator(Response response, Type type)
+        throws IOException, DecodeException, FeignException {
       BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8));
       return bufferedReader.lines().iterator();
     }
diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
index a3edc9800..f03e71f77 100644
--- a/core/src/main/java/feign/stream/StreamDecoder.java
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -16,7 +16,6 @@
 import feign.FeignException;
 import feign.Response;
 import feign.codec.Decoder;
-
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.ParameterizedType;
@@ -25,7 +24,6 @@
 import java.util.Spliterators;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
-
 import static feign.Util.ensureClosed;
 
 /**

From 48aedaa370d3f4234622bada4951800281cb0c78 Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Tue, 8 Mar 2022 14:10:00 +0800
Subject: [PATCH 05/29] Optimize StreamDecoder

---
 .../benchmark/DecoderIteratorsBenchmark.java  |  2 +-
 .../java/feign/stream/IteratorDecoder.java    | 65 -------------------
 .../main/java/feign/stream/StreamDecoder.java | 60 +++++++++--------
 .../java/feign/stream/StreamDecoderTest.java  | 36 +++++-----
 4 files changed, 54 insertions(+), 109 deletions(-)
 delete mode 100644 core/src/main/java/feign/stream/IteratorDecoder.java

diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
index 13415463b..36cf0c665 100644
--- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
+++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
@@ -99,7 +99,7 @@ public void buildDecoder() {
         type = new TypeReference>() {}.getType();
         break;
       case "stream":
-        decoder = StreamDecoder.create(new Decoder.Default());
+        decoder = StreamDecoder.create(JacksonIteratorDecoder.create());
         type = new TypeReference>() {}.getType();
         break;
       default:
diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java
deleted file mode 100644
index d330c16cc..000000000
--- a/core/src/main/java/feign/stream/IteratorDecoder.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2012-2022 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.stream;
-
-import feign.FeignException;
-import feign.Response;
-import feign.codec.DecodeException;
-import feign.codec.Decoder;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.lang.reflect.Type;
-import java.util.Iterator;
-import static feign.Util.UTF_8;
-
-/**
- * @author mroccyen
- */
-public interface IteratorDecoder extends Decoder {
-  /**
-   * Decodes an http response into an iterator. If you need to wrap exceptions, please do so via
-   * {@link DecodeException}.
-   *
-   * @param response the response to decode
-   * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the
-   *        method corresponding to this {@code response}.
-   * @return instance of {@code type}
-   * @throws IOException will be propagated safely to the caller.
-   * @throws DecodeException when decoding failed due to a checked exception besides IOException.
-   * @throws FeignException when decoding succeeds, but conveys the operation failed.
-   */
-  Iterator decodeIterator(Response response, Type type)
-      throws IOException, DecodeException, FeignException;
-
-  class LineToIteratorDecoder implements IteratorDecoder {
-
-    private final Decoder delegate;
-
-    public LineToIteratorDecoder(Decoder delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public Object decode(Response response, Type type) throws IOException {
-      return delegate.decode(response, type);
-    }
-
-    @Override
-    public Iterator decodeIterator(Response response, Type type)
-        throws IOException, DecodeException, FeignException {
-      BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8));
-      return bufferedReader.lines().iterator();
-    }
-  }
-}
diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
index f03e71f77..d3bfd12e5 100644
--- a/core/src/main/java/feign/stream/StreamDecoder.java
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -16,6 +16,7 @@
 import feign.FeignException;
 import feign.Response;
 import feign.codec.Decoder;
+import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.ParameterizedType;
@@ -24,6 +25,7 @@
 import java.util.Spliterators;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
+import static feign.Util.UTF_8;
 import static feign.Util.ensureClosed;
 
 /**
@@ -35,7 +37,7 @@
  * 
  * 
  * Feign.builder()
- *   .decoder(StreamDecoder.create(new Decoder.Default()))
+ *   .decoder(StreamDecoder.create(JacksonIteratorDecoder.create()))
  *   .doNotCloseAfterDecode() // Required for streaming
  *   .target(GitHub.class, "https://api.github.com");
  * interface GitHub {
@@ -48,28 +50,31 @@
  */
 public final class StreamDecoder implements Decoder {
 
-  private final Decoder delegate;
-  private IteratorDecoder iteratorDecoder;
+  private final Decoder iteratorDecoder;
 
-  public StreamDecoder(Decoder delegate) {
-    this.delegate = delegate;
+  public StreamDecoder(Decoder iteratorDecoder) {
+    this.iteratorDecoder = iteratorDecoder;
   }
 
   @Override
   public Object decode(Response response, Type type)
       throws IOException, FeignException {
     if (!isStream(type)) {
-      return delegate.decode(response, type);
+      return iteratorDecoder.decode(response, type);
     }
-    IteratorDecoder iteratorDecoder;
-    if (delegate instanceof IteratorDecoder) {
-      iteratorDecoder = (IteratorDecoder) delegate;
+    ParameterizedType streamType = (ParameterizedType) type;
+    Object result = iteratorDecoder
+        .decode(response, new IteratorParameterizedType(streamType));
+
+    Iterator iterator;
+    if (result instanceof Iterator) {
+      iterator = (Iterator) result;
     } else {
-      iteratorDecoder = getIteratorDecoder(delegate);
+      // use default iterator decoder handle
+      Object defaultResult = new LineToIteratorDecoder()
+          .decode(response, new IteratorParameterizedType(streamType));
+      iterator = (Iterator) defaultResult;
     }
-    ParameterizedType streamType = (ParameterizedType) type;
-    Iterator iterator = iteratorDecoder.decodeIterator(
-        response, new IteratorParameterizedType(streamType));
 
     return StreamSupport.stream(
         Spliterators.spliteratorUnknownSize(iterator, 0), false)
@@ -82,7 +87,7 @@ public Object decode(Response response, Type type)
         });
   }
 
-  private boolean isStream(Type type) {
+  public static boolean isStream(Type type) {
     if (!(type instanceof ParameterizedType)) {
       return false;
     }
@@ -90,19 +95,8 @@ private boolean isStream(Type type) {
     return parameterizedType.getRawType().equals(Stream.class);
   }
 
-  private IteratorDecoder getIteratorDecoder(Decoder delegate) {
-    synchronized (this) {
-      if (iteratorDecoder == null) {
-        synchronized (this) {
-          iteratorDecoder = new IteratorDecoder.LineToIteratorDecoder(delegate);
-        }
-      }
-    }
-    return iteratorDecoder;
-  }
-
-  public static StreamDecoder create(Decoder delegate) {
-    return new StreamDecoder(delegate);
+  public static StreamDecoder create(Decoder iteratorDecoder) {
+    return new StreamDecoder(iteratorDecoder);
   }
 
   static final class IteratorParameterizedType implements ParameterizedType {
@@ -129,4 +123,16 @@ public Type getOwnerType() {
     }
   }
 
+  /**
+   * Default implementation of Decodes an http response into an Iterator
+   */
+  public static class LineToIteratorDecoder implements Decoder {
+
+    @Override
+    public Object decode(Response response, Type type) throws IOException {
+      BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8));
+      return bufferedReader.lines().iterator();
+    }
+
+  }
 }
diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java
index 61bebebeb..9258f8c5e 100644
--- a/core/src/test/java/feign/stream/StreamDecoderTest.java
+++ b/core/src/test/java/feign/stream/StreamDecoderTest.java
@@ -21,6 +21,7 @@
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.junit.Test;
+import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.Type;
@@ -66,7 +67,24 @@ public void simpleStreamTest() {
     server.enqueue(new MockResponse().setBody("foo\nbar"));
 
     StreamInterface api = Feign.builder()
-        .decoder(StreamDecoder.create(new Decoder.Default()))
+        .decoder(StreamDecoder.create(
+            (response, type) -> new BufferedReader(response.body().asReader(UTF_8)).lines()
+                .iterator()))
+        .doNotCloseAfterDecode()
+        .target(StreamInterface.class, server.url("/").toString());
+
+    try (Stream stream = api.get()) {
+      assertThat(stream.collect(Collectors.toList())).isEqualTo(Arrays.asList("foo", "bar"));
+    }
+  }
+
+  @Test
+  public void simpleDefaultStreamTest() {
+    MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setBody("foo\nbar"));
+
+    StreamInterface api = Feign.builder()
+        .decoder(StreamDecoder.create((r, t) -> new Object()))
         .doNotCloseAfterDecode()
         .target(StreamInterface.class, server.url("/").toString());
 
@@ -86,7 +104,7 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
         .build();
 
     TestCloseableIterator it = new TestCloseableIterator();
-    StreamDecoder decoder = new StreamDecoder(new TestIteratorDecoder(it));
+    StreamDecoder decoder = new StreamDecoder((r, t) -> it);
 
     try (Stream stream =
         (Stream) decoder.decode(response, new TypeReference>() {}.getType())) {
@@ -97,20 +115,6 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
     }
   }
 
-  static class TestIteratorDecoder extends Decoder.Default implements IteratorDecoder {
-
-    final TestCloseableIterator testCloseableIterator;
-
-    TestIteratorDecoder(TestCloseableIterator testCloseableIterator) {
-      this.testCloseableIterator = testCloseableIterator;
-    }
-
-    @Override
-    public Iterator decodeIterator(Response response, Type type) throws FeignException {
-      return testCloseableIterator;
-    }
-  }
-
   static class TestCloseableIterator implements Iterator, Closeable {
     boolean called;
     boolean closed;

From 71b06f351f690e224f2e1978f01c3a847f56ef53 Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Mon, 21 Mar 2022 10:56:51 +0800
Subject: [PATCH 06/29] Optimize StreamDecoder

---
 .../main/java/feign/stream/StreamDecoder.java | 31 ++++++++++---------
 .../java/feign/stream/StreamDecoderTest.java  | 25 ++++++++++++---
 2 files changed, 36 insertions(+), 20 deletions(-)

diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
index d3bfd12e5..58b8d5a2d 100644
--- a/core/src/main/java/feign/stream/StreamDecoder.java
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -51,30 +51,27 @@
 public final class StreamDecoder implements Decoder {
 
   private final Decoder iteratorDecoder;
+  private final Decoder delegateDecoder;
 
-  public StreamDecoder(Decoder iteratorDecoder) {
+  StreamDecoder(Decoder iteratorDecoder, Decoder delegateDecoder) {
     this.iteratorDecoder = iteratorDecoder;
+    this.delegateDecoder = delegateDecoder;
   }
 
   @Override
   public Object decode(Response response, Type type)
       throws IOException, FeignException {
     if (!isStream(type)) {
-      return iteratorDecoder.decode(response, type);
+      if (delegateDecoder == null) {
+        throw new IllegalArgumentException("StreamDecoder supports types other than stream. " +
+            "When type is not stream, the delegate decoder needs to be setting.");
+      } else {
+        return delegateDecoder.decode(response, type);
+      }
     }
     ParameterizedType streamType = (ParameterizedType) type;
-    Object result = iteratorDecoder
-        .decode(response, new IteratorParameterizedType(streamType));
-
-    Iterator iterator;
-    if (result instanceof Iterator) {
-      iterator = (Iterator) result;
-    } else {
-      // use default iterator decoder handle
-      Object defaultResult = new LineToIteratorDecoder()
-          .decode(response, new IteratorParameterizedType(streamType));
-      iterator = (Iterator) defaultResult;
-    }
+    Iterator iterator =
+        (Iterator) iteratorDecoder.decode(response, new IteratorParameterizedType(streamType));
 
     return StreamSupport.stream(
         Spliterators.spliteratorUnknownSize(iterator, 0), false)
@@ -96,7 +93,11 @@ public static boolean isStream(Type type) {
   }
 
   public static StreamDecoder create(Decoder iteratorDecoder) {
-    return new StreamDecoder(iteratorDecoder);
+    return new StreamDecoder(iteratorDecoder, null);
+  }
+
+  public static StreamDecoder create(Decoder iteratorDecoder, Decoder delegateDecoder) {
+    return new StreamDecoder(iteratorDecoder, delegateDecoder);
   }
 
   static final class IteratorParameterizedType implements ParameterizedType {
diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java
index 9258f8c5e..56805df61 100644
--- a/core/src/test/java/feign/stream/StreamDecoderTest.java
+++ b/core/src/test/java/feign/stream/StreamDecoderTest.java
@@ -16,15 +16,12 @@
 import com.fasterxml.jackson.core.type.TypeReference;
 import feign.*;
 import feign.Request.HttpMethod;
-import feign.codec.DecodeException;
-import feign.codec.Decoder;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.junit.Test;
 import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
-import java.lang.reflect.Type;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
@@ -40,6 +37,9 @@ interface StreamInterface {
     @RequestLine("GET /")
     Stream get();
 
+    @RequestLine("GET /str")
+    String str();
+
     @RequestLine("GET /cars")
     Stream getCars();
 
@@ -84,7 +84,7 @@ public void simpleDefaultStreamTest() {
     server.enqueue(new MockResponse().setBody("foo\nbar"));
 
     StreamInterface api = Feign.builder()
-        .decoder(StreamDecoder.create((r, t) -> new Object()))
+        .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder()))
         .doNotCloseAfterDecode()
         .target(StreamInterface.class, server.url("/").toString());
 
@@ -93,6 +93,21 @@ public void simpleDefaultStreamTest() {
     }
   }
 
+  @Test
+  public void simpleDeleteDecoderTest() {
+    MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setBody("foo\nbar"));
+
+    StreamInterface api = Feign.builder()
+        .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder(), (r, t) -> "str"))
+        // .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder()))
+        .doNotCloseAfterDecode()
+        .target(StreamInterface.class, server.url("/").toString());
+
+    String str = api.str();
+    assertThat(str).isEqualTo("str");
+  }
+
   @Test
   public void shouldCloseIteratorWhenStreamClosed() throws IOException {
     Response response = Response.builder()
@@ -104,7 +119,7 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
         .build();
 
     TestCloseableIterator it = new TestCloseableIterator();
-    StreamDecoder decoder = new StreamDecoder((r, t) -> it);
+    StreamDecoder decoder = StreamDecoder.create((r, t) -> it);
 
     try (Stream stream =
         (Stream) decoder.decode(response, new TypeReference>() {}.getType())) {

From 8ddfa4d247f59e285e51d26a1b30a82a1a03e044 Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Mon, 7 Mar 2022 15:50:46 +0800
Subject: [PATCH 07/29] Optimize StreamDecoder

---
 .../benchmark/DecoderIteratorsBenchmark.java  |  2 +-
 .../main/java/feign/stream/StreamDecoder.java | 42 ++++++++++++++-----
 .../java/feign/stream/StreamDecoderTest.java  | 21 ++++------
 3 files changed, 40 insertions(+), 25 deletions(-)

diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
index 36cf0c665..a27bf3bcc 100644
--- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
+++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
@@ -99,7 +99,7 @@ public void buildDecoder() {
         type = new TypeReference>() {}.getType();
         break;
       case "stream":
-        decoder = StreamDecoder.create(JacksonIteratorDecoder.create());
+        decoder = StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create());
         type = new TypeReference>() {}.getType();
         break;
       default:
diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
index c5e796395..a9c47415d 100644
--- a/core/src/main/java/feign/stream/StreamDecoder.java
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -16,6 +16,7 @@
 import feign.FeignException;
 import feign.Response;
 import feign.codec.Decoder;
+import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.ParameterizedType;
@@ -24,6 +25,7 @@
 import java.util.Spliterators;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
+import static feign.Util.UTF_8;
 import static feign.Util.ensureClosed;
 
 /**
@@ -35,7 +37,7 @@
  * 
  * 
  * Feign.builder()
- *   .decoder(StreamDecoder.create(JacksonIteratorDecoder.create()))
+ *   .decoder(StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()))
  *   .doNotCloseAfterDecode() // Required for streaming
  *   .target(GitHub.class, "https://api.github.com");
  * interface GitHub {
@@ -43,27 +45,28 @@
  *   Stream contributors(@Param("owner") String owner, @Param("repo") String repo);
  * }
  * 
+ * + * @author mroccyen */ public final class StreamDecoder implements Decoder { + private final Decoder delegate; private final Decoder iteratorDecoder; - StreamDecoder(Decoder iteratorDecoder) { + StreamDecoder(Decoder delegate, Decoder iteratorDecoder) { + this.delegate = delegate; this.iteratorDecoder = iteratorDecoder; } @Override public Object decode(Response response, Type type) throws IOException, FeignException { - if (!(type instanceof ParameterizedType)) { - throw new IllegalArgumentException("StreamDecoder supports only stream: unknown " + type); + if (!isStream(type)) { + return delegate.decode(response, type); } ParameterizedType streamType = (ParameterizedType) type; - if (!Stream.class.equals(streamType.getRawType())) { - throw new IllegalArgumentException("StreamDecoder supports only stream: unknown " + type); - } - Iterator iterator = - (Iterator) iteratorDecoder.decode(response, new IteratorParameterizedType(streamType)); + Iterator iterator = (Iterator) iteratorDecoder.decode( + response, new IteratorParameterizedType(streamType)); return StreamSupport.stream( Spliterators.spliteratorUnknownSize(iterator, 0), false) @@ -76,8 +79,16 @@ public Object decode(Response response, Type type) }); } - public static StreamDecoder create(Decoder iteratorDecoder) { - return new StreamDecoder(iteratorDecoder); + private boolean isStream(Type type) { + if (!(type instanceof ParameterizedType)) { + return false; + } + ParameterizedType parameterizedType = (ParameterizedType) type; + return parameterizedType.getRawType().equals(Stream.class); + } + + public static StreamDecoder create(Decoder delegate, Decoder iteratorDecoder) { + return new StreamDecoder(delegate, iteratorDecoder); } static final class IteratorParameterizedType implements ParameterizedType { @@ -103,4 +114,13 @@ public Type getOwnerType() { return null; } } + + public static class DefaultIteratorDecoder implements Decoder { + @Override + public Object decode(Response response, Type type) throws IOException { + BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + } + } + } diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java index 0a9f94e62..13c8d965a 100644 --- a/core/src/test/java/feign/stream/StreamDecoderTest.java +++ b/core/src/test/java/feign/stream/StreamDecoderTest.java @@ -14,13 +14,12 @@ package feign.stream; import com.fasterxml.jackson.core.type.TypeReference; -import feign.Feign; -import feign.Request; +import feign.*; import feign.Request.HttpMethod; -import feign.RequestLine; -import feign.Response; -import feign.Util; -import java.io.BufferedReader; +import feign.codec.Decoder; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Test; import java.io.Closeable; import java.io.IOException; import java.util.Arrays; @@ -28,9 +27,6 @@ import java.util.Iterator; import java.util.stream.Collectors; import java.util.stream.Stream; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.Test; import static feign.Util.UTF_8; import static org.assertj.core.api.Assertions.assertThat; @@ -68,9 +64,8 @@ public void simpleStreamTest() { server.enqueue(new MockResponse().setBody("foo\nbar")); StreamInterface api = Feign.builder() - .decoder(StreamDecoder.create( - (response, type) -> new BufferedReader(response.body().asReader(UTF_8)).lines() - .iterator())) + .decoder(StreamDecoder.create(new Decoder.Default(), + new StreamDecoder.DefaultIteratorDecoder())) .doNotCloseAfterDecode() .target(StreamInterface.class, server.url("/").toString()); @@ -90,7 +85,7 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException { .build(); TestCloseableIterator it = new TestCloseableIterator(); - StreamDecoder decoder = new StreamDecoder((r, t) -> it); + StreamDecoder decoder = new StreamDecoder(new Decoder.Default(), (r, t) -> it); try (Stream stream = (Stream) decoder.decode(response, new TypeReference>() {}.getType())) { From 4531e4495b7ffd4a1abe781d5dfa028e727761e0 Mon Sep 17 00:00:00 2001 From: mroccyen Date: Tue, 8 Mar 2022 11:40:59 +0800 Subject: [PATCH 08/29] Optimize StreamDecoder --- .../benchmark/DecoderIteratorsBenchmark.java | 2 +- .../java/feign/stream/IteratorDecoder.java | 52 +++++++++++++++++++ .../main/java/feign/stream/StreamDecoder.java | 42 +++++++++------ .../java/feign/stream/StreamDecoderTest.java | 23 ++++++-- 4 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 core/src/main/java/feign/stream/IteratorDecoder.java diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java index a27bf3bcc..13415463b 100644 --- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java +++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java @@ -99,7 +99,7 @@ public void buildDecoder() { type = new TypeReference>() {}.getType(); break; case "stream": - decoder = StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()); + decoder = StreamDecoder.create(new Decoder.Default()); type = new TypeReference>() {}.getType(); break; default: diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java new file mode 100644 index 000000000..18a757c94 --- /dev/null +++ b/core/src/main/java/feign/stream/IteratorDecoder.java @@ -0,0 +1,52 @@ +package feign.stream; + +import feign.FeignException; +import feign.Response; +import feign.codec.DecodeException; +import feign.codec.Decoder; + +import java.io.BufferedReader; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Iterator; + +import static feign.Util.UTF_8; + +/** + * @author mroccyen + */ +public interface IteratorDecoder extends Decoder { + /** + * Decodes an http response into an iterator. If you need to + * wrap exceptions, please do so via {@link DecodeException}. + * + * @param response the response to decode + * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the + * method corresponding to this {@code response}. + * @return instance of {@code type} + * @throws IOException will be propagated safely to the caller. + * @throws DecodeException when decoding failed due to a checked exception besides IOException. + * @throws FeignException when decoding succeeds, but conveys the operation failed. + */ + Iterator decodeIterator(Response response, Type type) throws IOException, DecodeException, FeignException; + + class LineToIteratorDecoder implements IteratorDecoder { + + private final Decoder delegate; + + public LineToIteratorDecoder(Decoder delegate) { + this.delegate = delegate; + } + + @Override + public Object decode(Response response, Type type) throws IOException { + return delegate.decode(response, type); + } + + @Override + public Iterator decodeIterator(Response response, Type type) throws IOException, DecodeException, FeignException { + BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + } + } +} diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java index a9c47415d..a3edc9800 100644 --- a/core/src/main/java/feign/stream/StreamDecoder.java +++ b/core/src/main/java/feign/stream/StreamDecoder.java @@ -16,7 +16,7 @@ import feign.FeignException; import feign.Response; import feign.codec.Decoder; -import java.io.BufferedReader; + import java.io.Closeable; import java.io.IOException; import java.lang.reflect.ParameterizedType; @@ -25,7 +25,7 @@ import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static feign.Util.UTF_8; + import static feign.Util.ensureClosed; /** @@ -37,7 +37,7 @@ *
  * 
  * Feign.builder()
- *   .decoder(StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()))
+ *   .decoder(StreamDecoder.create(new Decoder.Default()))
  *   .doNotCloseAfterDecode() // Required for streaming
  *   .target(GitHub.class, "https://api.github.com");
  * interface GitHub {
@@ -51,11 +51,10 @@
 public final class StreamDecoder implements Decoder {
 
   private final Decoder delegate;
-  private final Decoder iteratorDecoder;
+  private IteratorDecoder iteratorDecoder;
 
-  StreamDecoder(Decoder delegate, Decoder iteratorDecoder) {
+  public StreamDecoder(Decoder delegate) {
     this.delegate = delegate;
-    this.iteratorDecoder = iteratorDecoder;
   }
 
   @Override
@@ -64,8 +63,14 @@ public Object decode(Response response, Type type)
     if (!isStream(type)) {
       return delegate.decode(response, type);
     }
+    IteratorDecoder iteratorDecoder;
+    if (delegate instanceof IteratorDecoder) {
+      iteratorDecoder = (IteratorDecoder) delegate;
+    } else {
+      iteratorDecoder = getIteratorDecoder(delegate);
+    }
     ParameterizedType streamType = (ParameterizedType) type;
-    Iterator iterator = (Iterator) iteratorDecoder.decode(
+    Iterator iterator = iteratorDecoder.decodeIterator(
         response, new IteratorParameterizedType(streamType));
 
     return StreamSupport.stream(
@@ -87,8 +92,19 @@ private boolean isStream(Type type) {
     return parameterizedType.getRawType().equals(Stream.class);
   }
 
-  public static StreamDecoder create(Decoder delegate, Decoder iteratorDecoder) {
-    return new StreamDecoder(delegate, iteratorDecoder);
+  private IteratorDecoder getIteratorDecoder(Decoder delegate) {
+    synchronized (this) {
+      if (iteratorDecoder == null) {
+        synchronized (this) {
+          iteratorDecoder = new IteratorDecoder.LineToIteratorDecoder(delegate);
+        }
+      }
+    }
+    return iteratorDecoder;
+  }
+
+  public static StreamDecoder create(Decoder delegate) {
+    return new StreamDecoder(delegate);
   }
 
   static final class IteratorParameterizedType implements ParameterizedType {
@@ -115,12 +131,4 @@ public Type getOwnerType() {
     }
   }
 
-  public static class DefaultIteratorDecoder implements Decoder {
-    @Override
-    public Object decode(Response response, Type type) throws IOException {
-      BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8));
-      return bufferedReader.lines().iterator();
-    }
-  }
-
 }
diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java
index 13c8d965a..61bebebeb 100644
--- a/core/src/test/java/feign/stream/StreamDecoderTest.java
+++ b/core/src/test/java/feign/stream/StreamDecoderTest.java
@@ -16,12 +16,14 @@
 import com.fasterxml.jackson.core.type.TypeReference;
 import feign.*;
 import feign.Request.HttpMethod;
+import feign.codec.DecodeException;
 import feign.codec.Decoder;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.junit.Test;
 import java.io.Closeable;
 import java.io.IOException;
+import java.lang.reflect.Type;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
@@ -64,8 +66,7 @@ public void simpleStreamTest() {
     server.enqueue(new MockResponse().setBody("foo\nbar"));
 
     StreamInterface api = Feign.builder()
-        .decoder(StreamDecoder.create(new Decoder.Default(),
-            new StreamDecoder.DefaultIteratorDecoder()))
+        .decoder(StreamDecoder.create(new Decoder.Default()))
         .doNotCloseAfterDecode()
         .target(StreamInterface.class, server.url("/").toString());
 
@@ -85,10 +86,10 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
         .build();
 
     TestCloseableIterator it = new TestCloseableIterator();
-    StreamDecoder decoder = new StreamDecoder(new Decoder.Default(), (r, t) -> it);
+    StreamDecoder decoder = new StreamDecoder(new TestIteratorDecoder(it));
 
     try (Stream stream =
-        (Stream) decoder.decode(response, new TypeReference>() {}.getType())) {
+        (Stream) decoder.decode(response, new TypeReference>() {}.getType())) {
       assertThat(stream.collect(Collectors.toList())).hasSize(1);
       assertThat(it.called).isTrue();
     } finally {
@@ -96,6 +97,20 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
     }
   }
 
+  static class TestIteratorDecoder extends Decoder.Default implements IteratorDecoder {
+
+    final TestCloseableIterator testCloseableIterator;
+
+    TestIteratorDecoder(TestCloseableIterator testCloseableIterator) {
+      this.testCloseableIterator = testCloseableIterator;
+    }
+
+    @Override
+    public Iterator decodeIterator(Response response, Type type) throws FeignException {
+      return testCloseableIterator;
+    }
+  }
+
   static class TestCloseableIterator implements Iterator, Closeable {
     boolean called;
     boolean closed;

From a8af9b14c0e80ed4edff43a6a09cde96094dc461 Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Tue, 8 Mar 2022 11:49:30 +0800
Subject: [PATCH 09/29] add license header

---
 .../src/main/java/feign/stream/IteratorDecoder.java | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java
index 18a757c94..4ae114c9a 100644
--- a/core/src/main/java/feign/stream/IteratorDecoder.java
+++ b/core/src/main/java/feign/stream/IteratorDecoder.java
@@ -1,3 +1,16 @@
+/*
+ * Copyright 2012-2022 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.stream;
 
 import feign.FeignException;

From 932a52750465504d7b07005918daaec69ab114a1 Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Tue, 8 Mar 2022 11:54:50 +0800
Subject: [PATCH 10/29] add license header

---
 .../java/feign/stream/IteratorDecoder.java    | 20 +++++++++----------
 .../main/java/feign/stream/StreamDecoder.java |  2 --
 2 files changed, 10 insertions(+), 12 deletions(-)

diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java
index 4ae114c9a..d330c16cc 100644
--- a/core/src/main/java/feign/stream/IteratorDecoder.java
+++ b/core/src/main/java/feign/stream/IteratorDecoder.java
@@ -17,12 +17,10 @@
 import feign.Response;
 import feign.codec.DecodeException;
 import feign.codec.Decoder;
-
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.lang.reflect.Type;
 import java.util.Iterator;
-
 import static feign.Util.UTF_8;
 
 /**
@@ -30,18 +28,19 @@
  */
 public interface IteratorDecoder extends Decoder {
   /**
-   * Decodes an http response into an iterator. If you need to
-   * wrap exceptions, please do so via {@link DecodeException}.
+   * Decodes an http response into an iterator. If you need to wrap exceptions, please do so via
+   * {@link DecodeException}.
    *
    * @param response the response to decode
-   * @param type     {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the
-   *                 method corresponding to this {@code response}.
+   * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the
+   *        method corresponding to this {@code response}.
    * @return instance of {@code type}
-   * @throws IOException     will be propagated safely to the caller.
+   * @throws IOException will be propagated safely to the caller.
    * @throws DecodeException when decoding failed due to a checked exception besides IOException.
-   * @throws FeignException  when decoding succeeds, but conveys the operation failed.
+   * @throws FeignException when decoding succeeds, but conveys the operation failed.
    */
-  Iterator decodeIterator(Response response, Type type) throws IOException, DecodeException, FeignException;
+  Iterator decodeIterator(Response response, Type type)
+      throws IOException, DecodeException, FeignException;
 
   class LineToIteratorDecoder implements IteratorDecoder {
 
@@ -57,7 +56,8 @@ public Object decode(Response response, Type type) throws IOException {
     }
 
     @Override
-    public Iterator decodeIterator(Response response, Type type) throws IOException, DecodeException, FeignException {
+    public Iterator decodeIterator(Response response, Type type)
+        throws IOException, DecodeException, FeignException {
       BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8));
       return bufferedReader.lines().iterator();
     }
diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
index a3edc9800..f03e71f77 100644
--- a/core/src/main/java/feign/stream/StreamDecoder.java
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -16,7 +16,6 @@
 import feign.FeignException;
 import feign.Response;
 import feign.codec.Decoder;
-
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.ParameterizedType;
@@ -25,7 +24,6 @@
 import java.util.Spliterators;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
-
 import static feign.Util.ensureClosed;
 
 /**

From 3e47367c578143ff10077cce62283f67189ac0bd Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Tue, 8 Mar 2022 14:10:00 +0800
Subject: [PATCH 11/29] Optimize StreamDecoder

---
 .../benchmark/DecoderIteratorsBenchmark.java  |  2 +-
 .../java/feign/stream/IteratorDecoder.java    | 65 -------------------
 .../main/java/feign/stream/StreamDecoder.java | 60 +++++++++--------
 .../java/feign/stream/StreamDecoderTest.java  | 36 +++++-----
 4 files changed, 54 insertions(+), 109 deletions(-)
 delete mode 100644 core/src/main/java/feign/stream/IteratorDecoder.java

diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
index 13415463b..36cf0c665 100644
--- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
+++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
@@ -99,7 +99,7 @@ public void buildDecoder() {
         type = new TypeReference>() {}.getType();
         break;
       case "stream":
-        decoder = StreamDecoder.create(new Decoder.Default());
+        decoder = StreamDecoder.create(JacksonIteratorDecoder.create());
         type = new TypeReference>() {}.getType();
         break;
       default:
diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java
deleted file mode 100644
index d330c16cc..000000000
--- a/core/src/main/java/feign/stream/IteratorDecoder.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2012-2022 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.stream;
-
-import feign.FeignException;
-import feign.Response;
-import feign.codec.DecodeException;
-import feign.codec.Decoder;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.lang.reflect.Type;
-import java.util.Iterator;
-import static feign.Util.UTF_8;
-
-/**
- * @author mroccyen
- */
-public interface IteratorDecoder extends Decoder {
-  /**
-   * Decodes an http response into an iterator. If you need to wrap exceptions, please do so via
-   * {@link DecodeException}.
-   *
-   * @param response the response to decode
-   * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the
-   *        method corresponding to this {@code response}.
-   * @return instance of {@code type}
-   * @throws IOException will be propagated safely to the caller.
-   * @throws DecodeException when decoding failed due to a checked exception besides IOException.
-   * @throws FeignException when decoding succeeds, but conveys the operation failed.
-   */
-  Iterator decodeIterator(Response response, Type type)
-      throws IOException, DecodeException, FeignException;
-
-  class LineToIteratorDecoder implements IteratorDecoder {
-
-    private final Decoder delegate;
-
-    public LineToIteratorDecoder(Decoder delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public Object decode(Response response, Type type) throws IOException {
-      return delegate.decode(response, type);
-    }
-
-    @Override
-    public Iterator decodeIterator(Response response, Type type)
-        throws IOException, DecodeException, FeignException {
-      BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8));
-      return bufferedReader.lines().iterator();
-    }
-  }
-}
diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
index f03e71f77..d3bfd12e5 100644
--- a/core/src/main/java/feign/stream/StreamDecoder.java
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -16,6 +16,7 @@
 import feign.FeignException;
 import feign.Response;
 import feign.codec.Decoder;
+import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.ParameterizedType;
@@ -24,6 +25,7 @@
 import java.util.Spliterators;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
+import static feign.Util.UTF_8;
 import static feign.Util.ensureClosed;
 
 /**
@@ -35,7 +37,7 @@
  * 
  * 
  * Feign.builder()
- *   .decoder(StreamDecoder.create(new Decoder.Default()))
+ *   .decoder(StreamDecoder.create(JacksonIteratorDecoder.create()))
  *   .doNotCloseAfterDecode() // Required for streaming
  *   .target(GitHub.class, "https://api.github.com");
  * interface GitHub {
@@ -48,28 +50,31 @@
  */
 public final class StreamDecoder implements Decoder {
 
-  private final Decoder delegate;
-  private IteratorDecoder iteratorDecoder;
+  private final Decoder iteratorDecoder;
 
-  public StreamDecoder(Decoder delegate) {
-    this.delegate = delegate;
+  public StreamDecoder(Decoder iteratorDecoder) {
+    this.iteratorDecoder = iteratorDecoder;
   }
 
   @Override
   public Object decode(Response response, Type type)
       throws IOException, FeignException {
     if (!isStream(type)) {
-      return delegate.decode(response, type);
+      return iteratorDecoder.decode(response, type);
     }
-    IteratorDecoder iteratorDecoder;
-    if (delegate instanceof IteratorDecoder) {
-      iteratorDecoder = (IteratorDecoder) delegate;
+    ParameterizedType streamType = (ParameterizedType) type;
+    Object result = iteratorDecoder
+        .decode(response, new IteratorParameterizedType(streamType));
+
+    Iterator iterator;
+    if (result instanceof Iterator) {
+      iterator = (Iterator) result;
     } else {
-      iteratorDecoder = getIteratorDecoder(delegate);
+      // use default iterator decoder handle
+      Object defaultResult = new LineToIteratorDecoder()
+          .decode(response, new IteratorParameterizedType(streamType));
+      iterator = (Iterator) defaultResult;
     }
-    ParameterizedType streamType = (ParameterizedType) type;
-    Iterator iterator = iteratorDecoder.decodeIterator(
-        response, new IteratorParameterizedType(streamType));
 
     return StreamSupport.stream(
         Spliterators.spliteratorUnknownSize(iterator, 0), false)
@@ -82,7 +87,7 @@ public Object decode(Response response, Type type)
         });
   }
 
-  private boolean isStream(Type type) {
+  public static boolean isStream(Type type) {
     if (!(type instanceof ParameterizedType)) {
       return false;
     }
@@ -90,19 +95,8 @@ private boolean isStream(Type type) {
     return parameterizedType.getRawType().equals(Stream.class);
   }
 
-  private IteratorDecoder getIteratorDecoder(Decoder delegate) {
-    synchronized (this) {
-      if (iteratorDecoder == null) {
-        synchronized (this) {
-          iteratorDecoder = new IteratorDecoder.LineToIteratorDecoder(delegate);
-        }
-      }
-    }
-    return iteratorDecoder;
-  }
-
-  public static StreamDecoder create(Decoder delegate) {
-    return new StreamDecoder(delegate);
+  public static StreamDecoder create(Decoder iteratorDecoder) {
+    return new StreamDecoder(iteratorDecoder);
   }
 
   static final class IteratorParameterizedType implements ParameterizedType {
@@ -129,4 +123,16 @@ public Type getOwnerType() {
     }
   }
 
+  /**
+   * Default implementation of Decodes an http response into an Iterator
+   */
+  public static class LineToIteratorDecoder implements Decoder {
+
+    @Override
+    public Object decode(Response response, Type type) throws IOException {
+      BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8));
+      return bufferedReader.lines().iterator();
+    }
+
+  }
 }
diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java
index 61bebebeb..9258f8c5e 100644
--- a/core/src/test/java/feign/stream/StreamDecoderTest.java
+++ b/core/src/test/java/feign/stream/StreamDecoderTest.java
@@ -21,6 +21,7 @@
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.junit.Test;
+import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.Type;
@@ -66,7 +67,24 @@ public void simpleStreamTest() {
     server.enqueue(new MockResponse().setBody("foo\nbar"));
 
     StreamInterface api = Feign.builder()
-        .decoder(StreamDecoder.create(new Decoder.Default()))
+        .decoder(StreamDecoder.create(
+            (response, type) -> new BufferedReader(response.body().asReader(UTF_8)).lines()
+                .iterator()))
+        .doNotCloseAfterDecode()
+        .target(StreamInterface.class, server.url("/").toString());
+
+    try (Stream stream = api.get()) {
+      assertThat(stream.collect(Collectors.toList())).isEqualTo(Arrays.asList("foo", "bar"));
+    }
+  }
+
+  @Test
+  public void simpleDefaultStreamTest() {
+    MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setBody("foo\nbar"));
+
+    StreamInterface api = Feign.builder()
+        .decoder(StreamDecoder.create((r, t) -> new Object()))
         .doNotCloseAfterDecode()
         .target(StreamInterface.class, server.url("/").toString());
 
@@ -86,7 +104,7 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
         .build();
 
     TestCloseableIterator it = new TestCloseableIterator();
-    StreamDecoder decoder = new StreamDecoder(new TestIteratorDecoder(it));
+    StreamDecoder decoder = new StreamDecoder((r, t) -> it);
 
     try (Stream stream =
         (Stream) decoder.decode(response, new TypeReference>() {}.getType())) {
@@ -97,20 +115,6 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
     }
   }
 
-  static class TestIteratorDecoder extends Decoder.Default implements IteratorDecoder {
-
-    final TestCloseableIterator testCloseableIterator;
-
-    TestIteratorDecoder(TestCloseableIterator testCloseableIterator) {
-      this.testCloseableIterator = testCloseableIterator;
-    }
-
-    @Override
-    public Iterator decodeIterator(Response response, Type type) throws FeignException {
-      return testCloseableIterator;
-    }
-  }
-
   static class TestCloseableIterator implements Iterator, Closeable {
     boolean called;
     boolean closed;

From 3378e13ca99b60d21fccb31d69102b8f6e29cc61 Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Mon, 21 Mar 2022 10:56:51 +0800
Subject: [PATCH 12/29] Optimize StreamDecoder

---
 .../main/java/feign/stream/StreamDecoder.java | 31 ++++++++++---------
 .../java/feign/stream/StreamDecoderTest.java  | 25 ++++++++++++---
 2 files changed, 36 insertions(+), 20 deletions(-)

diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
index d3bfd12e5..58b8d5a2d 100644
--- a/core/src/main/java/feign/stream/StreamDecoder.java
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -51,30 +51,27 @@
 public final class StreamDecoder implements Decoder {
 
   private final Decoder iteratorDecoder;
+  private final Decoder delegateDecoder;
 
-  public StreamDecoder(Decoder iteratorDecoder) {
+  StreamDecoder(Decoder iteratorDecoder, Decoder delegateDecoder) {
     this.iteratorDecoder = iteratorDecoder;
+    this.delegateDecoder = delegateDecoder;
   }
 
   @Override
   public Object decode(Response response, Type type)
       throws IOException, FeignException {
     if (!isStream(type)) {
-      return iteratorDecoder.decode(response, type);
+      if (delegateDecoder == null) {
+        throw new IllegalArgumentException("StreamDecoder supports types other than stream. " +
+            "When type is not stream, the delegate decoder needs to be setting.");
+      } else {
+        return delegateDecoder.decode(response, type);
+      }
     }
     ParameterizedType streamType = (ParameterizedType) type;
-    Object result = iteratorDecoder
-        .decode(response, new IteratorParameterizedType(streamType));
-
-    Iterator iterator;
-    if (result instanceof Iterator) {
-      iterator = (Iterator) result;
-    } else {
-      // use default iterator decoder handle
-      Object defaultResult = new LineToIteratorDecoder()
-          .decode(response, new IteratorParameterizedType(streamType));
-      iterator = (Iterator) defaultResult;
-    }
+    Iterator iterator =
+        (Iterator) iteratorDecoder.decode(response, new IteratorParameterizedType(streamType));
 
     return StreamSupport.stream(
         Spliterators.spliteratorUnknownSize(iterator, 0), false)
@@ -96,7 +93,11 @@ public static boolean isStream(Type type) {
   }
 
   public static StreamDecoder create(Decoder iteratorDecoder) {
-    return new StreamDecoder(iteratorDecoder);
+    return new StreamDecoder(iteratorDecoder, null);
+  }
+
+  public static StreamDecoder create(Decoder iteratorDecoder, Decoder delegateDecoder) {
+    return new StreamDecoder(iteratorDecoder, delegateDecoder);
   }
 
   static final class IteratorParameterizedType implements ParameterizedType {
diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java
index 9258f8c5e..56805df61 100644
--- a/core/src/test/java/feign/stream/StreamDecoderTest.java
+++ b/core/src/test/java/feign/stream/StreamDecoderTest.java
@@ -16,15 +16,12 @@
 import com.fasterxml.jackson.core.type.TypeReference;
 import feign.*;
 import feign.Request.HttpMethod;
-import feign.codec.DecodeException;
-import feign.codec.Decoder;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.junit.Test;
 import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
-import java.lang.reflect.Type;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
@@ -40,6 +37,9 @@ interface StreamInterface {
     @RequestLine("GET /")
     Stream get();
 
+    @RequestLine("GET /str")
+    String str();
+
     @RequestLine("GET /cars")
     Stream getCars();
 
@@ -84,7 +84,7 @@ public void simpleDefaultStreamTest() {
     server.enqueue(new MockResponse().setBody("foo\nbar"));
 
     StreamInterface api = Feign.builder()
-        .decoder(StreamDecoder.create((r, t) -> new Object()))
+        .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder()))
         .doNotCloseAfterDecode()
         .target(StreamInterface.class, server.url("/").toString());
 
@@ -93,6 +93,21 @@ public void simpleDefaultStreamTest() {
     }
   }
 
+  @Test
+  public void simpleDeleteDecoderTest() {
+    MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setBody("foo\nbar"));
+
+    StreamInterface api = Feign.builder()
+        .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder(), (r, t) -> "str"))
+        // .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder()))
+        .doNotCloseAfterDecode()
+        .target(StreamInterface.class, server.url("/").toString());
+
+    String str = api.str();
+    assertThat(str).isEqualTo("str");
+  }
+
   @Test
   public void shouldCloseIteratorWhenStreamClosed() throws IOException {
     Response response = Response.builder()
@@ -104,7 +119,7 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
         .build();
 
     TestCloseableIterator it = new TestCloseableIterator();
-    StreamDecoder decoder = new StreamDecoder((r, t) -> it);
+    StreamDecoder decoder = StreamDecoder.create((r, t) -> it);
 
     try (Stream stream =
         (Stream) decoder.decode(response, new TypeReference>() {}.getType())) {

From 131e67a3afd5d54e3e4a862a3118a16d2b617b89 Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Fri, 25 Mar 2022 19:13:19 +0800
Subject: [PATCH 13/29] Optimize StreamDecoder

---
 .../main/java/feign/stream/StreamDecoder.java   | 17 -----------------
 .../java/feign/stream/StreamDecoderTest.java    | 10 ++++++++--
 2 files changed, 8 insertions(+), 19 deletions(-)

diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
index 58b8d5a2d..827d9a132 100644
--- a/core/src/main/java/feign/stream/StreamDecoder.java
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -16,7 +16,6 @@
 import feign.FeignException;
 import feign.Response;
 import feign.codec.Decoder;
-import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.ParameterizedType;
@@ -25,7 +24,6 @@
 import java.util.Spliterators;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
-import static feign.Util.UTF_8;
 import static feign.Util.ensureClosed;
 
 /**
@@ -45,8 +43,6 @@
  *   Stream contributors(@Param("owner") String owner, @Param("repo") String repo);
  * }
  * 
- * - * @author mroccyen */ public final class StreamDecoder implements Decoder { @@ -123,17 +119,4 @@ public Type getOwnerType() { return null; } } - - /** - * Default implementation of Decodes an http response into an Iterator - */ - public static class LineToIteratorDecoder implements Decoder { - - @Override - public Object decode(Response response, Type type) throws IOException { - BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8)); - return bufferedReader.lines().iterator(); - } - - } } diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java index 56805df61..697d6f837 100644 --- a/core/src/test/java/feign/stream/StreamDecoderTest.java +++ b/core/src/test/java/feign/stream/StreamDecoderTest.java @@ -84,7 +84,10 @@ public void simpleDefaultStreamTest() { server.enqueue(new MockResponse().setBody("foo\nbar")); StreamInterface api = Feign.builder() - .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder())) + .decoder(StreamDecoder.create((r, t) -> { + BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + })) .doNotCloseAfterDecode() .target(StreamInterface.class, server.url("/").toString()); @@ -99,7 +102,10 @@ public void simpleDeleteDecoderTest() { server.enqueue(new MockResponse().setBody("foo\nbar")); StreamInterface api = Feign.builder() - .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder(), (r, t) -> "str")) + .decoder(StreamDecoder.create((r, t) -> { + BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + }, (r, t) -> "str")) // .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder())) .doNotCloseAfterDecode() .target(StreamInterface.class, server.url("/").toString()); From 3d039a1f341cbd1229b54cad1282a5cdd04d9b5c Mon Sep 17 00:00:00 2001 From: mroccyen Date: Mon, 7 Mar 2022 15:50:46 +0800 Subject: [PATCH 14/29] Optimize StreamDecoder --- .../benchmark/DecoderIteratorsBenchmark.java | 2 +- .../main/java/feign/stream/StreamDecoder.java | 42 ++++++++++++++----- .../java/feign/stream/StreamDecoderTest.java | 21 ++++------ 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java index 36cf0c665..a27bf3bcc 100644 --- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java +++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java @@ -99,7 +99,7 @@ public void buildDecoder() { type = new TypeReference>() {}.getType(); break; case "stream": - decoder = StreamDecoder.create(JacksonIteratorDecoder.create()); + decoder = StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()); type = new TypeReference>() {}.getType(); break; default: diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java index c5e796395..a9c47415d 100644 --- a/core/src/main/java/feign/stream/StreamDecoder.java +++ b/core/src/main/java/feign/stream/StreamDecoder.java @@ -16,6 +16,7 @@ import feign.FeignException; import feign.Response; import feign.codec.Decoder; +import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.lang.reflect.ParameterizedType; @@ -24,6 +25,7 @@ import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import static feign.Util.UTF_8; import static feign.Util.ensureClosed; /** @@ -35,7 +37,7 @@ *
  * 
  * Feign.builder()
- *   .decoder(StreamDecoder.create(JacksonIteratorDecoder.create()))
+ *   .decoder(StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()))
  *   .doNotCloseAfterDecode() // Required for streaming
  *   .target(GitHub.class, "https://api.github.com");
  * interface GitHub {
@@ -43,27 +45,28 @@
  *   Stream contributors(@Param("owner") String owner, @Param("repo") String repo);
  * }
  * 
+ * + * @author mroccyen */ public final class StreamDecoder implements Decoder { + private final Decoder delegate; private final Decoder iteratorDecoder; - StreamDecoder(Decoder iteratorDecoder) { + StreamDecoder(Decoder delegate, Decoder iteratorDecoder) { + this.delegate = delegate; this.iteratorDecoder = iteratorDecoder; } @Override public Object decode(Response response, Type type) throws IOException, FeignException { - if (!(type instanceof ParameterizedType)) { - throw new IllegalArgumentException("StreamDecoder supports only stream: unknown " + type); + if (!isStream(type)) { + return delegate.decode(response, type); } ParameterizedType streamType = (ParameterizedType) type; - if (!Stream.class.equals(streamType.getRawType())) { - throw new IllegalArgumentException("StreamDecoder supports only stream: unknown " + type); - } - Iterator iterator = - (Iterator) iteratorDecoder.decode(response, new IteratorParameterizedType(streamType)); + Iterator iterator = (Iterator) iteratorDecoder.decode( + response, new IteratorParameterizedType(streamType)); return StreamSupport.stream( Spliterators.spliteratorUnknownSize(iterator, 0), false) @@ -76,8 +79,16 @@ public Object decode(Response response, Type type) }); } - public static StreamDecoder create(Decoder iteratorDecoder) { - return new StreamDecoder(iteratorDecoder); + private boolean isStream(Type type) { + if (!(type instanceof ParameterizedType)) { + return false; + } + ParameterizedType parameterizedType = (ParameterizedType) type; + return parameterizedType.getRawType().equals(Stream.class); + } + + public static StreamDecoder create(Decoder delegate, Decoder iteratorDecoder) { + return new StreamDecoder(delegate, iteratorDecoder); } static final class IteratorParameterizedType implements ParameterizedType { @@ -103,4 +114,13 @@ public Type getOwnerType() { return null; } } + + public static class DefaultIteratorDecoder implements Decoder { + @Override + public Object decode(Response response, Type type) throws IOException { + BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + } + } + } diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java index 0a9f94e62..13c8d965a 100644 --- a/core/src/test/java/feign/stream/StreamDecoderTest.java +++ b/core/src/test/java/feign/stream/StreamDecoderTest.java @@ -14,13 +14,12 @@ package feign.stream; import com.fasterxml.jackson.core.type.TypeReference; -import feign.Feign; -import feign.Request; +import feign.*; import feign.Request.HttpMethod; -import feign.RequestLine; -import feign.Response; -import feign.Util; -import java.io.BufferedReader; +import feign.codec.Decoder; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Test; import java.io.Closeable; import java.io.IOException; import java.util.Arrays; @@ -28,9 +27,6 @@ import java.util.Iterator; import java.util.stream.Collectors; import java.util.stream.Stream; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.Test; import static feign.Util.UTF_8; import static org.assertj.core.api.Assertions.assertThat; @@ -68,9 +64,8 @@ public void simpleStreamTest() { server.enqueue(new MockResponse().setBody("foo\nbar")); StreamInterface api = Feign.builder() - .decoder(StreamDecoder.create( - (response, type) -> new BufferedReader(response.body().asReader(UTF_8)).lines() - .iterator())) + .decoder(StreamDecoder.create(new Decoder.Default(), + new StreamDecoder.DefaultIteratorDecoder())) .doNotCloseAfterDecode() .target(StreamInterface.class, server.url("/").toString()); @@ -90,7 +85,7 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException { .build(); TestCloseableIterator it = new TestCloseableIterator(); - StreamDecoder decoder = new StreamDecoder((r, t) -> it); + StreamDecoder decoder = new StreamDecoder(new Decoder.Default(), (r, t) -> it); try (Stream stream = (Stream) decoder.decode(response, new TypeReference>() {}.getType())) { From 55c125049a0b86bf655758bb8aa90e3568dfd5fa Mon Sep 17 00:00:00 2001 From: mroccyen Date: Tue, 8 Mar 2022 11:40:59 +0800 Subject: [PATCH 15/29] Optimize StreamDecoder --- .../benchmark/DecoderIteratorsBenchmark.java | 2 +- .../java/feign/stream/IteratorDecoder.java | 52 +++++++++++++++++++ .../main/java/feign/stream/StreamDecoder.java | 42 +++++++++------ .../java/feign/stream/StreamDecoderTest.java | 23 ++++++-- 4 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 core/src/main/java/feign/stream/IteratorDecoder.java diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java index a27bf3bcc..13415463b 100644 --- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java +++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java @@ -99,7 +99,7 @@ public void buildDecoder() { type = new TypeReference>() {}.getType(); break; case "stream": - decoder = StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()); + decoder = StreamDecoder.create(new Decoder.Default()); type = new TypeReference>() {}.getType(); break; default: diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java new file mode 100644 index 000000000..18a757c94 --- /dev/null +++ b/core/src/main/java/feign/stream/IteratorDecoder.java @@ -0,0 +1,52 @@ +package feign.stream; + +import feign.FeignException; +import feign.Response; +import feign.codec.DecodeException; +import feign.codec.Decoder; + +import java.io.BufferedReader; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Iterator; + +import static feign.Util.UTF_8; + +/** + * @author mroccyen + */ +public interface IteratorDecoder extends Decoder { + /** + * Decodes an http response into an iterator. If you need to + * wrap exceptions, please do so via {@link DecodeException}. + * + * @param response the response to decode + * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the + * method corresponding to this {@code response}. + * @return instance of {@code type} + * @throws IOException will be propagated safely to the caller. + * @throws DecodeException when decoding failed due to a checked exception besides IOException. + * @throws FeignException when decoding succeeds, but conveys the operation failed. + */ + Iterator decodeIterator(Response response, Type type) throws IOException, DecodeException, FeignException; + + class LineToIteratorDecoder implements IteratorDecoder { + + private final Decoder delegate; + + public LineToIteratorDecoder(Decoder delegate) { + this.delegate = delegate; + } + + @Override + public Object decode(Response response, Type type) throws IOException { + return delegate.decode(response, type); + } + + @Override + public Iterator decodeIterator(Response response, Type type) throws IOException, DecodeException, FeignException { + BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + } + } +} diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java index a9c47415d..a3edc9800 100644 --- a/core/src/main/java/feign/stream/StreamDecoder.java +++ b/core/src/main/java/feign/stream/StreamDecoder.java @@ -16,7 +16,7 @@ import feign.FeignException; import feign.Response; import feign.codec.Decoder; -import java.io.BufferedReader; + import java.io.Closeable; import java.io.IOException; import java.lang.reflect.ParameterizedType; @@ -25,7 +25,7 @@ import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static feign.Util.UTF_8; + import static feign.Util.ensureClosed; /** @@ -37,7 +37,7 @@ *
  * 
  * Feign.builder()
- *   .decoder(StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()))
+ *   .decoder(StreamDecoder.create(new Decoder.Default()))
  *   .doNotCloseAfterDecode() // Required for streaming
  *   .target(GitHub.class, "https://api.github.com");
  * interface GitHub {
@@ -51,11 +51,10 @@
 public final class StreamDecoder implements Decoder {
 
   private final Decoder delegate;
-  private final Decoder iteratorDecoder;
+  private IteratorDecoder iteratorDecoder;
 
-  StreamDecoder(Decoder delegate, Decoder iteratorDecoder) {
+  public StreamDecoder(Decoder delegate) {
     this.delegate = delegate;
-    this.iteratorDecoder = iteratorDecoder;
   }
 
   @Override
@@ -64,8 +63,14 @@ public Object decode(Response response, Type type)
     if (!isStream(type)) {
       return delegate.decode(response, type);
     }
+    IteratorDecoder iteratorDecoder;
+    if (delegate instanceof IteratorDecoder) {
+      iteratorDecoder = (IteratorDecoder) delegate;
+    } else {
+      iteratorDecoder = getIteratorDecoder(delegate);
+    }
     ParameterizedType streamType = (ParameterizedType) type;
-    Iterator iterator = (Iterator) iteratorDecoder.decode(
+    Iterator iterator = iteratorDecoder.decodeIterator(
         response, new IteratorParameterizedType(streamType));
 
     return StreamSupport.stream(
@@ -87,8 +92,19 @@ private boolean isStream(Type type) {
     return parameterizedType.getRawType().equals(Stream.class);
   }
 
-  public static StreamDecoder create(Decoder delegate, Decoder iteratorDecoder) {
-    return new StreamDecoder(delegate, iteratorDecoder);
+  private IteratorDecoder getIteratorDecoder(Decoder delegate) {
+    synchronized (this) {
+      if (iteratorDecoder == null) {
+        synchronized (this) {
+          iteratorDecoder = new IteratorDecoder.LineToIteratorDecoder(delegate);
+        }
+      }
+    }
+    return iteratorDecoder;
+  }
+
+  public static StreamDecoder create(Decoder delegate) {
+    return new StreamDecoder(delegate);
   }
 
   static final class IteratorParameterizedType implements ParameterizedType {
@@ -115,12 +131,4 @@ public Type getOwnerType() {
     }
   }
 
-  public static class DefaultIteratorDecoder implements Decoder {
-    @Override
-    public Object decode(Response response, Type type) throws IOException {
-      BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8));
-      return bufferedReader.lines().iterator();
-    }
-  }
-
 }
diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java
index 13c8d965a..61bebebeb 100644
--- a/core/src/test/java/feign/stream/StreamDecoderTest.java
+++ b/core/src/test/java/feign/stream/StreamDecoderTest.java
@@ -16,12 +16,14 @@
 import com.fasterxml.jackson.core.type.TypeReference;
 import feign.*;
 import feign.Request.HttpMethod;
+import feign.codec.DecodeException;
 import feign.codec.Decoder;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.junit.Test;
 import java.io.Closeable;
 import java.io.IOException;
+import java.lang.reflect.Type;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
@@ -64,8 +66,7 @@ public void simpleStreamTest() {
     server.enqueue(new MockResponse().setBody("foo\nbar"));
 
     StreamInterface api = Feign.builder()
-        .decoder(StreamDecoder.create(new Decoder.Default(),
-            new StreamDecoder.DefaultIteratorDecoder()))
+        .decoder(StreamDecoder.create(new Decoder.Default()))
         .doNotCloseAfterDecode()
         .target(StreamInterface.class, server.url("/").toString());
 
@@ -85,10 +86,10 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
         .build();
 
     TestCloseableIterator it = new TestCloseableIterator();
-    StreamDecoder decoder = new StreamDecoder(new Decoder.Default(), (r, t) -> it);
+    StreamDecoder decoder = new StreamDecoder(new TestIteratorDecoder(it));
 
     try (Stream stream =
-        (Stream) decoder.decode(response, new TypeReference>() {}.getType())) {
+        (Stream) decoder.decode(response, new TypeReference>() {}.getType())) {
       assertThat(stream.collect(Collectors.toList())).hasSize(1);
       assertThat(it.called).isTrue();
     } finally {
@@ -96,6 +97,20 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
     }
   }
 
+  static class TestIteratorDecoder extends Decoder.Default implements IteratorDecoder {
+
+    final TestCloseableIterator testCloseableIterator;
+
+    TestIteratorDecoder(TestCloseableIterator testCloseableIterator) {
+      this.testCloseableIterator = testCloseableIterator;
+    }
+
+    @Override
+    public Iterator decodeIterator(Response response, Type type) throws FeignException {
+      return testCloseableIterator;
+    }
+  }
+
   static class TestCloseableIterator implements Iterator, Closeable {
     boolean called;
     boolean closed;

From 05704829c706f048e47939d2570d31110fe3b009 Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Tue, 8 Mar 2022 11:49:30 +0800
Subject: [PATCH 16/29] add license header

---
 .../src/main/java/feign/stream/IteratorDecoder.java | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java
index 18a757c94..4ae114c9a 100644
--- a/core/src/main/java/feign/stream/IteratorDecoder.java
+++ b/core/src/main/java/feign/stream/IteratorDecoder.java
@@ -1,3 +1,16 @@
+/*
+ * Copyright 2012-2022 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.stream;
 
 import feign.FeignException;

From 7dd94e2356eabe3eb446933d4fc7669300dd211c Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Tue, 8 Mar 2022 11:54:50 +0800
Subject: [PATCH 17/29] add license header

---
 .../java/feign/stream/IteratorDecoder.java    | 20 +++++++++----------
 .../main/java/feign/stream/StreamDecoder.java |  2 --
 2 files changed, 10 insertions(+), 12 deletions(-)

diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java
index 4ae114c9a..d330c16cc 100644
--- a/core/src/main/java/feign/stream/IteratorDecoder.java
+++ b/core/src/main/java/feign/stream/IteratorDecoder.java
@@ -17,12 +17,10 @@
 import feign.Response;
 import feign.codec.DecodeException;
 import feign.codec.Decoder;
-
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.lang.reflect.Type;
 import java.util.Iterator;
-
 import static feign.Util.UTF_8;
 
 /**
@@ -30,18 +28,19 @@
  */
 public interface IteratorDecoder extends Decoder {
   /**
-   * Decodes an http response into an iterator. If you need to
-   * wrap exceptions, please do so via {@link DecodeException}.
+   * Decodes an http response into an iterator. If you need to wrap exceptions, please do so via
+   * {@link DecodeException}.
    *
    * @param response the response to decode
-   * @param type     {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the
-   *                 method corresponding to this {@code response}.
+   * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the
+   *        method corresponding to this {@code response}.
    * @return instance of {@code type}
-   * @throws IOException     will be propagated safely to the caller.
+   * @throws IOException will be propagated safely to the caller.
    * @throws DecodeException when decoding failed due to a checked exception besides IOException.
-   * @throws FeignException  when decoding succeeds, but conveys the operation failed.
+   * @throws FeignException when decoding succeeds, but conveys the operation failed.
    */
-  Iterator decodeIterator(Response response, Type type) throws IOException, DecodeException, FeignException;
+  Iterator decodeIterator(Response response, Type type)
+      throws IOException, DecodeException, FeignException;
 
   class LineToIteratorDecoder implements IteratorDecoder {
 
@@ -57,7 +56,8 @@ public Object decode(Response response, Type type) throws IOException {
     }
 
     @Override
-    public Iterator decodeIterator(Response response, Type type) throws IOException, DecodeException, FeignException {
+    public Iterator decodeIterator(Response response, Type type)
+        throws IOException, DecodeException, FeignException {
       BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8));
       return bufferedReader.lines().iterator();
     }
diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
index a3edc9800..f03e71f77 100644
--- a/core/src/main/java/feign/stream/StreamDecoder.java
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -16,7 +16,6 @@
 import feign.FeignException;
 import feign.Response;
 import feign.codec.Decoder;
-
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.ParameterizedType;
@@ -25,7 +24,6 @@
 import java.util.Spliterators;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
-
 import static feign.Util.ensureClosed;
 
 /**

From 5b97437961c73594821e944350b553be75b06225 Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Tue, 8 Mar 2022 14:10:00 +0800
Subject: [PATCH 18/29] Optimize StreamDecoder

---
 .../benchmark/DecoderIteratorsBenchmark.java  |  2 +-
 .../java/feign/stream/IteratorDecoder.java    | 65 -------------------
 .../main/java/feign/stream/StreamDecoder.java | 60 +++++++++--------
 .../java/feign/stream/StreamDecoderTest.java  | 36 +++++-----
 4 files changed, 54 insertions(+), 109 deletions(-)
 delete mode 100644 core/src/main/java/feign/stream/IteratorDecoder.java

diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
index 13415463b..36cf0c665 100644
--- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
+++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java
@@ -99,7 +99,7 @@ public void buildDecoder() {
         type = new TypeReference>() {}.getType();
         break;
       case "stream":
-        decoder = StreamDecoder.create(new Decoder.Default());
+        decoder = StreamDecoder.create(JacksonIteratorDecoder.create());
         type = new TypeReference>() {}.getType();
         break;
       default:
diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java
deleted file mode 100644
index d330c16cc..000000000
--- a/core/src/main/java/feign/stream/IteratorDecoder.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2012-2022 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.stream;
-
-import feign.FeignException;
-import feign.Response;
-import feign.codec.DecodeException;
-import feign.codec.Decoder;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.lang.reflect.Type;
-import java.util.Iterator;
-import static feign.Util.UTF_8;
-
-/**
- * @author mroccyen
- */
-public interface IteratorDecoder extends Decoder {
-  /**
-   * Decodes an http response into an iterator. If you need to wrap exceptions, please do so via
-   * {@link DecodeException}.
-   *
-   * @param response the response to decode
-   * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the
-   *        method corresponding to this {@code response}.
-   * @return instance of {@code type}
-   * @throws IOException will be propagated safely to the caller.
-   * @throws DecodeException when decoding failed due to a checked exception besides IOException.
-   * @throws FeignException when decoding succeeds, but conveys the operation failed.
-   */
-  Iterator decodeIterator(Response response, Type type)
-      throws IOException, DecodeException, FeignException;
-
-  class LineToIteratorDecoder implements IteratorDecoder {
-
-    private final Decoder delegate;
-
-    public LineToIteratorDecoder(Decoder delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public Object decode(Response response, Type type) throws IOException {
-      return delegate.decode(response, type);
-    }
-
-    @Override
-    public Iterator decodeIterator(Response response, Type type)
-        throws IOException, DecodeException, FeignException {
-      BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8));
-      return bufferedReader.lines().iterator();
-    }
-  }
-}
diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
index f03e71f77..d3bfd12e5 100644
--- a/core/src/main/java/feign/stream/StreamDecoder.java
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -16,6 +16,7 @@
 import feign.FeignException;
 import feign.Response;
 import feign.codec.Decoder;
+import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.ParameterizedType;
@@ -24,6 +25,7 @@
 import java.util.Spliterators;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
+import static feign.Util.UTF_8;
 import static feign.Util.ensureClosed;
 
 /**
@@ -35,7 +37,7 @@
  * 
  * 
  * Feign.builder()
- *   .decoder(StreamDecoder.create(new Decoder.Default()))
+ *   .decoder(StreamDecoder.create(JacksonIteratorDecoder.create()))
  *   .doNotCloseAfterDecode() // Required for streaming
  *   .target(GitHub.class, "https://api.github.com");
  * interface GitHub {
@@ -48,28 +50,31 @@
  */
 public final class StreamDecoder implements Decoder {
 
-  private final Decoder delegate;
-  private IteratorDecoder iteratorDecoder;
+  private final Decoder iteratorDecoder;
 
-  public StreamDecoder(Decoder delegate) {
-    this.delegate = delegate;
+  public StreamDecoder(Decoder iteratorDecoder) {
+    this.iteratorDecoder = iteratorDecoder;
   }
 
   @Override
   public Object decode(Response response, Type type)
       throws IOException, FeignException {
     if (!isStream(type)) {
-      return delegate.decode(response, type);
+      return iteratorDecoder.decode(response, type);
     }
-    IteratorDecoder iteratorDecoder;
-    if (delegate instanceof IteratorDecoder) {
-      iteratorDecoder = (IteratorDecoder) delegate;
+    ParameterizedType streamType = (ParameterizedType) type;
+    Object result = iteratorDecoder
+        .decode(response, new IteratorParameterizedType(streamType));
+
+    Iterator iterator;
+    if (result instanceof Iterator) {
+      iterator = (Iterator) result;
     } else {
-      iteratorDecoder = getIteratorDecoder(delegate);
+      // use default iterator decoder handle
+      Object defaultResult = new LineToIteratorDecoder()
+          .decode(response, new IteratorParameterizedType(streamType));
+      iterator = (Iterator) defaultResult;
     }
-    ParameterizedType streamType = (ParameterizedType) type;
-    Iterator iterator = iteratorDecoder.decodeIterator(
-        response, new IteratorParameterizedType(streamType));
 
     return StreamSupport.stream(
         Spliterators.spliteratorUnknownSize(iterator, 0), false)
@@ -82,7 +87,7 @@ public Object decode(Response response, Type type)
         });
   }
 
-  private boolean isStream(Type type) {
+  public static boolean isStream(Type type) {
     if (!(type instanceof ParameterizedType)) {
       return false;
     }
@@ -90,19 +95,8 @@ private boolean isStream(Type type) {
     return parameterizedType.getRawType().equals(Stream.class);
   }
 
-  private IteratorDecoder getIteratorDecoder(Decoder delegate) {
-    synchronized (this) {
-      if (iteratorDecoder == null) {
-        synchronized (this) {
-          iteratorDecoder = new IteratorDecoder.LineToIteratorDecoder(delegate);
-        }
-      }
-    }
-    return iteratorDecoder;
-  }
-
-  public static StreamDecoder create(Decoder delegate) {
-    return new StreamDecoder(delegate);
+  public static StreamDecoder create(Decoder iteratorDecoder) {
+    return new StreamDecoder(iteratorDecoder);
   }
 
   static final class IteratorParameterizedType implements ParameterizedType {
@@ -129,4 +123,16 @@ public Type getOwnerType() {
     }
   }
 
+  /**
+   * Default implementation of Decodes an http response into an Iterator
+   */
+  public static class LineToIteratorDecoder implements Decoder {
+
+    @Override
+    public Object decode(Response response, Type type) throws IOException {
+      BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8));
+      return bufferedReader.lines().iterator();
+    }
+
+  }
 }
diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java
index 61bebebeb..9258f8c5e 100644
--- a/core/src/test/java/feign/stream/StreamDecoderTest.java
+++ b/core/src/test/java/feign/stream/StreamDecoderTest.java
@@ -21,6 +21,7 @@
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.junit.Test;
+import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.Type;
@@ -66,7 +67,24 @@ public void simpleStreamTest() {
     server.enqueue(new MockResponse().setBody("foo\nbar"));
 
     StreamInterface api = Feign.builder()
-        .decoder(StreamDecoder.create(new Decoder.Default()))
+        .decoder(StreamDecoder.create(
+            (response, type) -> new BufferedReader(response.body().asReader(UTF_8)).lines()
+                .iterator()))
+        .doNotCloseAfterDecode()
+        .target(StreamInterface.class, server.url("/").toString());
+
+    try (Stream stream = api.get()) {
+      assertThat(stream.collect(Collectors.toList())).isEqualTo(Arrays.asList("foo", "bar"));
+    }
+  }
+
+  @Test
+  public void simpleDefaultStreamTest() {
+    MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setBody("foo\nbar"));
+
+    StreamInterface api = Feign.builder()
+        .decoder(StreamDecoder.create((r, t) -> new Object()))
         .doNotCloseAfterDecode()
         .target(StreamInterface.class, server.url("/").toString());
 
@@ -86,7 +104,7 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
         .build();
 
     TestCloseableIterator it = new TestCloseableIterator();
-    StreamDecoder decoder = new StreamDecoder(new TestIteratorDecoder(it));
+    StreamDecoder decoder = new StreamDecoder((r, t) -> it);
 
     try (Stream stream =
         (Stream) decoder.decode(response, new TypeReference>() {}.getType())) {
@@ -97,20 +115,6 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
     }
   }
 
-  static class TestIteratorDecoder extends Decoder.Default implements IteratorDecoder {
-
-    final TestCloseableIterator testCloseableIterator;
-
-    TestIteratorDecoder(TestCloseableIterator testCloseableIterator) {
-      this.testCloseableIterator = testCloseableIterator;
-    }
-
-    @Override
-    public Iterator decodeIterator(Response response, Type type) throws FeignException {
-      return testCloseableIterator;
-    }
-  }
-
   static class TestCloseableIterator implements Iterator, Closeable {
     boolean called;
     boolean closed;

From 07a8b2bfbfab2ac50db7ffc4d3814fd8cb23b1e9 Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Mon, 21 Mar 2022 10:56:51 +0800
Subject: [PATCH 19/29] Optimize StreamDecoder

---
 .../main/java/feign/stream/StreamDecoder.java | 31 ++++++++++---------
 .../java/feign/stream/StreamDecoderTest.java  | 25 ++++++++++++---
 2 files changed, 36 insertions(+), 20 deletions(-)

diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
index d3bfd12e5..58b8d5a2d 100644
--- a/core/src/main/java/feign/stream/StreamDecoder.java
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -51,30 +51,27 @@
 public final class StreamDecoder implements Decoder {
 
   private final Decoder iteratorDecoder;
+  private final Decoder delegateDecoder;
 
-  public StreamDecoder(Decoder iteratorDecoder) {
+  StreamDecoder(Decoder iteratorDecoder, Decoder delegateDecoder) {
     this.iteratorDecoder = iteratorDecoder;
+    this.delegateDecoder = delegateDecoder;
   }
 
   @Override
   public Object decode(Response response, Type type)
       throws IOException, FeignException {
     if (!isStream(type)) {
-      return iteratorDecoder.decode(response, type);
+      if (delegateDecoder == null) {
+        throw new IllegalArgumentException("StreamDecoder supports types other than stream. " +
+            "When type is not stream, the delegate decoder needs to be setting.");
+      } else {
+        return delegateDecoder.decode(response, type);
+      }
     }
     ParameterizedType streamType = (ParameterizedType) type;
-    Object result = iteratorDecoder
-        .decode(response, new IteratorParameterizedType(streamType));
-
-    Iterator iterator;
-    if (result instanceof Iterator) {
-      iterator = (Iterator) result;
-    } else {
-      // use default iterator decoder handle
-      Object defaultResult = new LineToIteratorDecoder()
-          .decode(response, new IteratorParameterizedType(streamType));
-      iterator = (Iterator) defaultResult;
-    }
+    Iterator iterator =
+        (Iterator) iteratorDecoder.decode(response, new IteratorParameterizedType(streamType));
 
     return StreamSupport.stream(
         Spliterators.spliteratorUnknownSize(iterator, 0), false)
@@ -96,7 +93,11 @@ public static boolean isStream(Type type) {
   }
 
   public static StreamDecoder create(Decoder iteratorDecoder) {
-    return new StreamDecoder(iteratorDecoder);
+    return new StreamDecoder(iteratorDecoder, null);
+  }
+
+  public static StreamDecoder create(Decoder iteratorDecoder, Decoder delegateDecoder) {
+    return new StreamDecoder(iteratorDecoder, delegateDecoder);
   }
 
   static final class IteratorParameterizedType implements ParameterizedType {
diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java
index 9258f8c5e..56805df61 100644
--- a/core/src/test/java/feign/stream/StreamDecoderTest.java
+++ b/core/src/test/java/feign/stream/StreamDecoderTest.java
@@ -16,15 +16,12 @@
 import com.fasterxml.jackson.core.type.TypeReference;
 import feign.*;
 import feign.Request.HttpMethod;
-import feign.codec.DecodeException;
-import feign.codec.Decoder;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.junit.Test;
 import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
-import java.lang.reflect.Type;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
@@ -40,6 +37,9 @@ interface StreamInterface {
     @RequestLine("GET /")
     Stream get();
 
+    @RequestLine("GET /str")
+    String str();
+
     @RequestLine("GET /cars")
     Stream getCars();
 
@@ -84,7 +84,7 @@ public void simpleDefaultStreamTest() {
     server.enqueue(new MockResponse().setBody("foo\nbar"));
 
     StreamInterface api = Feign.builder()
-        .decoder(StreamDecoder.create((r, t) -> new Object()))
+        .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder()))
         .doNotCloseAfterDecode()
         .target(StreamInterface.class, server.url("/").toString());
 
@@ -93,6 +93,21 @@ public void simpleDefaultStreamTest() {
     }
   }
 
+  @Test
+  public void simpleDeleteDecoderTest() {
+    MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setBody("foo\nbar"));
+
+    StreamInterface api = Feign.builder()
+        .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder(), (r, t) -> "str"))
+        // .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder()))
+        .doNotCloseAfterDecode()
+        .target(StreamInterface.class, server.url("/").toString());
+
+    String str = api.str();
+    assertThat(str).isEqualTo("str");
+  }
+
   @Test
   public void shouldCloseIteratorWhenStreamClosed() throws IOException {
     Response response = Response.builder()
@@ -104,7 +119,7 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException {
         .build();
 
     TestCloseableIterator it = new TestCloseableIterator();
-    StreamDecoder decoder = new StreamDecoder((r, t) -> it);
+    StreamDecoder decoder = StreamDecoder.create((r, t) -> it);
 
     try (Stream stream =
         (Stream) decoder.decode(response, new TypeReference>() {}.getType())) {

From 995abdcd9ac206826616694e43a69b7febb502a8 Mon Sep 17 00:00:00 2001
From: mroccyen 
Date: Fri, 25 Mar 2022 19:13:19 +0800
Subject: [PATCH 20/29] Optimize StreamDecoder

---
 .../main/java/feign/stream/StreamDecoder.java   | 17 -----------------
 .../java/feign/stream/StreamDecoderTest.java    | 10 ++++++++--
 2 files changed, 8 insertions(+), 19 deletions(-)

diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
index 58b8d5a2d..827d9a132 100644
--- a/core/src/main/java/feign/stream/StreamDecoder.java
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -16,7 +16,6 @@
 import feign.FeignException;
 import feign.Response;
 import feign.codec.Decoder;
-import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.reflect.ParameterizedType;
@@ -25,7 +24,6 @@
 import java.util.Spliterators;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
-import static feign.Util.UTF_8;
 import static feign.Util.ensureClosed;
 
 /**
@@ -45,8 +43,6 @@
  *   Stream contributors(@Param("owner") String owner, @Param("repo") String repo);
  * }
  * 
- * - * @author mroccyen */ public final class StreamDecoder implements Decoder { @@ -123,17 +119,4 @@ public Type getOwnerType() { return null; } } - - /** - * Default implementation of Decodes an http response into an Iterator - */ - public static class LineToIteratorDecoder implements Decoder { - - @Override - public Object decode(Response response, Type type) throws IOException { - BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8)); - return bufferedReader.lines().iterator(); - } - - } } diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java index 56805df61..697d6f837 100644 --- a/core/src/test/java/feign/stream/StreamDecoderTest.java +++ b/core/src/test/java/feign/stream/StreamDecoderTest.java @@ -84,7 +84,10 @@ public void simpleDefaultStreamTest() { server.enqueue(new MockResponse().setBody("foo\nbar")); StreamInterface api = Feign.builder() - .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder())) + .decoder(StreamDecoder.create((r, t) -> { + BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + })) .doNotCloseAfterDecode() .target(StreamInterface.class, server.url("/").toString()); @@ -99,7 +102,10 @@ public void simpleDeleteDecoderTest() { server.enqueue(new MockResponse().setBody("foo\nbar")); StreamInterface api = Feign.builder() - .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder(), (r, t) -> "str")) + .decoder(StreamDecoder.create((r, t) -> { + BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + }, (r, t) -> "str")) // .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder())) .doNotCloseAfterDecode() .target(StreamInterface.class, server.url("/").toString()); From 573338ac6616f7dadd324e32fe35bcaa55fcb5c0 Mon Sep 17 00:00:00 2001 From: mroccyen Date: Mon, 7 Mar 2022 15:50:46 +0800 Subject: [PATCH 21/29] Optimize StreamDecoder --- .../main/java/feign/benchmark/DecoderIteratorsBenchmark.java | 2 +- core/src/test/java/feign/stream/StreamDecoderTest.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java index 36cf0c665..a27bf3bcc 100644 --- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java +++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java @@ -99,7 +99,7 @@ public void buildDecoder() { type = new TypeReference>() {}.getType(); break; case "stream": - decoder = StreamDecoder.create(JacksonIteratorDecoder.create()); + decoder = StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()); type = new TypeReference>() {}.getType(); break; default: diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java index 697d6f837..fdb633a20 100644 --- a/core/src/test/java/feign/stream/StreamDecoderTest.java +++ b/core/src/test/java/feign/stream/StreamDecoderTest.java @@ -27,6 +27,9 @@ import java.util.Iterator; import java.util.stream.Collectors; import java.util.stream.Stream; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Test; import static feign.Util.UTF_8; import static org.assertj.core.api.Assertions.assertThat; From b0d550f122189bf02799bb653b52a147ca4b92bb Mon Sep 17 00:00:00 2001 From: mroccyen Date: Tue, 8 Mar 2022 11:49:30 +0800 Subject: [PATCH 22/29] add license header --- core/src/main/java/feign/stream/IteratorDecoder.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 core/src/main/java/feign/stream/IteratorDecoder.java diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java new file mode 100644 index 000000000..e69de29bb From c983eef2367d5f8e1ca9d2ac97dadba4f1cef57e Mon Sep 17 00:00:00 2001 From: mroccyen Date: Tue, 8 Mar 2022 14:10:00 +0800 Subject: [PATCH 23/29] Optimize StreamDecoder --- .../benchmark/DecoderIteratorsBenchmark.java | 2 +- .../java/feign/stream/IteratorDecoder.java | 0 .../main/java/feign/stream/StreamDecoder.java | 48 ++++++++++++------- .../java/feign/stream/StreamDecoderTest.java | 34 ++----------- 4 files changed, 38 insertions(+), 46 deletions(-) delete mode 100644 core/src/main/java/feign/stream/IteratorDecoder.java diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java index a27bf3bcc..36cf0c665 100644 --- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java +++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java @@ -99,7 +99,7 @@ public void buildDecoder() { type = new TypeReference>() {}.getType(); break; case "stream": - decoder = StreamDecoder.create(new Decoder.Default(), JacksonIteratorDecoder.create()); + decoder = StreamDecoder.create(JacksonIteratorDecoder.create()); type = new TypeReference>() {}.getType(); break; default: diff --git a/core/src/main/java/feign/stream/IteratorDecoder.java b/core/src/main/java/feign/stream/IteratorDecoder.java deleted file mode 100644 index e69de29bb..000000000 diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java index 827d9a132..d3bfd12e5 100644 --- a/core/src/main/java/feign/stream/StreamDecoder.java +++ b/core/src/main/java/feign/stream/StreamDecoder.java @@ -16,6 +16,7 @@ import feign.FeignException; import feign.Response; import feign.codec.Decoder; +import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.lang.reflect.ParameterizedType; @@ -24,6 +25,7 @@ import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import static feign.Util.UTF_8; import static feign.Util.ensureClosed; /** @@ -43,31 +45,36 @@ * Stream contributors(@Param("owner") String owner, @Param("repo") String repo); * }
*
+ * + * @author mroccyen */ public final class StreamDecoder implements Decoder { private final Decoder iteratorDecoder; - private final Decoder delegateDecoder; - StreamDecoder(Decoder iteratorDecoder, Decoder delegateDecoder) { + public StreamDecoder(Decoder iteratorDecoder) { this.iteratorDecoder = iteratorDecoder; - this.delegateDecoder = delegateDecoder; } @Override public Object decode(Response response, Type type) throws IOException, FeignException { if (!isStream(type)) { - if (delegateDecoder == null) { - throw new IllegalArgumentException("StreamDecoder supports types other than stream. " + - "When type is not stream, the delegate decoder needs to be setting."); - } else { - return delegateDecoder.decode(response, type); - } + return iteratorDecoder.decode(response, type); } ParameterizedType streamType = (ParameterizedType) type; - Iterator iterator = - (Iterator) iteratorDecoder.decode(response, new IteratorParameterizedType(streamType)); + Object result = iteratorDecoder + .decode(response, new IteratorParameterizedType(streamType)); + + Iterator iterator; + if (result instanceof Iterator) { + iterator = (Iterator) result; + } else { + // use default iterator decoder handle + Object defaultResult = new LineToIteratorDecoder() + .decode(response, new IteratorParameterizedType(streamType)); + iterator = (Iterator) defaultResult; + } return StreamSupport.stream( Spliterators.spliteratorUnknownSize(iterator, 0), false) @@ -89,11 +96,7 @@ public static boolean isStream(Type type) { } public static StreamDecoder create(Decoder iteratorDecoder) { - return new StreamDecoder(iteratorDecoder, null); - } - - public static StreamDecoder create(Decoder iteratorDecoder, Decoder delegateDecoder) { - return new StreamDecoder(iteratorDecoder, delegateDecoder); + return new StreamDecoder(iteratorDecoder); } static final class IteratorParameterizedType implements ParameterizedType { @@ -119,4 +122,17 @@ public Type getOwnerType() { return null; } } + + /** + * Default implementation of Decodes an http response into an Iterator + */ + public static class LineToIteratorDecoder implements Decoder { + + @Override + public Object decode(Response response, Type type) throws IOException { + BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + } + + } } diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java index fdb633a20..9258f8c5e 100644 --- a/core/src/test/java/feign/stream/StreamDecoderTest.java +++ b/core/src/test/java/feign/stream/StreamDecoderTest.java @@ -16,20 +16,20 @@ import com.fasterxml.jackson.core.type.TypeReference; import feign.*; import feign.Request.HttpMethod; +import feign.codec.DecodeException; +import feign.codec.Decoder; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.Test; import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; +import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.stream.Collectors; import java.util.stream.Stream; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.Test; import static feign.Util.UTF_8; import static org.assertj.core.api.Assertions.assertThat; @@ -40,9 +40,6 @@ interface StreamInterface { @RequestLine("GET /") Stream get(); - @RequestLine("GET /str") - String str(); - @RequestLine("GET /cars") Stream getCars(); @@ -87,10 +84,7 @@ public void simpleDefaultStreamTest() { server.enqueue(new MockResponse().setBody("foo\nbar")); StreamInterface api = Feign.builder() - .decoder(StreamDecoder.create((r, t) -> { - BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8)); - return bufferedReader.lines().iterator(); - })) + .decoder(StreamDecoder.create((r, t) -> new Object())) .doNotCloseAfterDecode() .target(StreamInterface.class, server.url("/").toString()); @@ -99,24 +93,6 @@ public void simpleDefaultStreamTest() { } } - @Test - public void simpleDeleteDecoderTest() { - MockWebServer server = new MockWebServer(); - server.enqueue(new MockResponse().setBody("foo\nbar")); - - StreamInterface api = Feign.builder() - .decoder(StreamDecoder.create((r, t) -> { - BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8)); - return bufferedReader.lines().iterator(); - }, (r, t) -> "str")) - // .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder())) - .doNotCloseAfterDecode() - .target(StreamInterface.class, server.url("/").toString()); - - String str = api.str(); - assertThat(str).isEqualTo("str"); - } - @Test public void shouldCloseIteratorWhenStreamClosed() throws IOException { Response response = Response.builder() @@ -128,7 +104,7 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException { .build(); TestCloseableIterator it = new TestCloseableIterator(); - StreamDecoder decoder = StreamDecoder.create((r, t) -> it); + StreamDecoder decoder = new StreamDecoder((r, t) -> it); try (Stream stream = (Stream) decoder.decode(response, new TypeReference>() {}.getType())) { From 9cb9808f33910f9a5748f9a088df620af3f5f38d Mon Sep 17 00:00:00 2001 From: mroccyen Date: Mon, 21 Mar 2022 10:56:51 +0800 Subject: [PATCH 24/29] Optimize StreamDecoder --- .../main/java/feign/stream/StreamDecoder.java | 31 ++++++++++--------- .../java/feign/stream/StreamDecoderTest.java | 25 ++++++++++++--- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java index d3bfd12e5..58b8d5a2d 100644 --- a/core/src/main/java/feign/stream/StreamDecoder.java +++ b/core/src/main/java/feign/stream/StreamDecoder.java @@ -51,30 +51,27 @@ public final class StreamDecoder implements Decoder { private final Decoder iteratorDecoder; + private final Decoder delegateDecoder; - public StreamDecoder(Decoder iteratorDecoder) { + StreamDecoder(Decoder iteratorDecoder, Decoder delegateDecoder) { this.iteratorDecoder = iteratorDecoder; + this.delegateDecoder = delegateDecoder; } @Override public Object decode(Response response, Type type) throws IOException, FeignException { if (!isStream(type)) { - return iteratorDecoder.decode(response, type); + if (delegateDecoder == null) { + throw new IllegalArgumentException("StreamDecoder supports types other than stream. " + + "When type is not stream, the delegate decoder needs to be setting."); + } else { + return delegateDecoder.decode(response, type); + } } ParameterizedType streamType = (ParameterizedType) type; - Object result = iteratorDecoder - .decode(response, new IteratorParameterizedType(streamType)); - - Iterator iterator; - if (result instanceof Iterator) { - iterator = (Iterator) result; - } else { - // use default iterator decoder handle - Object defaultResult = new LineToIteratorDecoder() - .decode(response, new IteratorParameterizedType(streamType)); - iterator = (Iterator) defaultResult; - } + Iterator iterator = + (Iterator) iteratorDecoder.decode(response, new IteratorParameterizedType(streamType)); return StreamSupport.stream( Spliterators.spliteratorUnknownSize(iterator, 0), false) @@ -96,7 +93,11 @@ public static boolean isStream(Type type) { } public static StreamDecoder create(Decoder iteratorDecoder) { - return new StreamDecoder(iteratorDecoder); + return new StreamDecoder(iteratorDecoder, null); + } + + public static StreamDecoder create(Decoder iteratorDecoder, Decoder delegateDecoder) { + return new StreamDecoder(iteratorDecoder, delegateDecoder); } static final class IteratorParameterizedType implements ParameterizedType { diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java index 9258f8c5e..56805df61 100644 --- a/core/src/test/java/feign/stream/StreamDecoderTest.java +++ b/core/src/test/java/feign/stream/StreamDecoderTest.java @@ -16,15 +16,12 @@ import com.fasterxml.jackson.core.type.TypeReference; import feign.*; import feign.Request.HttpMethod; -import feign.codec.DecodeException; -import feign.codec.Decoder; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.Test; import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; -import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; @@ -40,6 +37,9 @@ interface StreamInterface { @RequestLine("GET /") Stream get(); + @RequestLine("GET /str") + String str(); + @RequestLine("GET /cars") Stream getCars(); @@ -84,7 +84,7 @@ public void simpleDefaultStreamTest() { server.enqueue(new MockResponse().setBody("foo\nbar")); StreamInterface api = Feign.builder() - .decoder(StreamDecoder.create((r, t) -> new Object())) + .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder())) .doNotCloseAfterDecode() .target(StreamInterface.class, server.url("/").toString()); @@ -93,6 +93,21 @@ public void simpleDefaultStreamTest() { } } + @Test + public void simpleDeleteDecoderTest() { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("foo\nbar")); + + StreamInterface api = Feign.builder() + .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder(), (r, t) -> "str")) + // .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder())) + .doNotCloseAfterDecode() + .target(StreamInterface.class, server.url("/").toString()); + + String str = api.str(); + assertThat(str).isEqualTo("str"); + } + @Test public void shouldCloseIteratorWhenStreamClosed() throws IOException { Response response = Response.builder() @@ -104,7 +119,7 @@ public void shouldCloseIteratorWhenStreamClosed() throws IOException { .build(); TestCloseableIterator it = new TestCloseableIterator(); - StreamDecoder decoder = new StreamDecoder((r, t) -> it); + StreamDecoder decoder = StreamDecoder.create((r, t) -> it); try (Stream stream = (Stream) decoder.decode(response, new TypeReference>() {}.getType())) { From 8d8e64e412e9ada13f50dfee8b8c0eb5b945d627 Mon Sep 17 00:00:00 2001 From: mroccyen Date: Fri, 25 Mar 2022 19:29:00 +0800 Subject: [PATCH 25/29] Optimize StreamDecoder --- .../main/java/feign/stream/StreamDecoder.java | 17 ----------------- .../java/feign/stream/StreamDecoderTest.java | 1 - 2 files changed, 18 deletions(-) diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java index 58b8d5a2d..827d9a132 100644 --- a/core/src/main/java/feign/stream/StreamDecoder.java +++ b/core/src/main/java/feign/stream/StreamDecoder.java @@ -16,7 +16,6 @@ import feign.FeignException; import feign.Response; import feign.codec.Decoder; -import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.lang.reflect.ParameterizedType; @@ -25,7 +24,6 @@ import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static feign.Util.UTF_8; import static feign.Util.ensureClosed; /** @@ -45,8 +43,6 @@ * Stream contributors(@Param("owner") String owner, @Param("repo") String repo); * }
*
- * - * @author mroccyen */ public final class StreamDecoder implements Decoder { @@ -123,17 +119,4 @@ public Type getOwnerType() { return null; } } - - /** - * Default implementation of Decodes an http response into an Iterator - */ - public static class LineToIteratorDecoder implements Decoder { - - @Override - public Object decode(Response response, Type type) throws IOException { - BufferedReader bufferedReader = new BufferedReader(response.body().asReader(UTF_8)); - return bufferedReader.lines().iterator(); - } - - } } diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java index 697d6f837..a19f8a17e 100644 --- a/core/src/test/java/feign/stream/StreamDecoderTest.java +++ b/core/src/test/java/feign/stream/StreamDecoderTest.java @@ -106,7 +106,6 @@ public void simpleDeleteDecoderTest() { BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8)); return bufferedReader.lines().iterator(); }, (r, t) -> "str")) - // .decoder(StreamDecoder.create(new StreamDecoder.LineToIteratorDecoder())) .doNotCloseAfterDecode() .target(StreamInterface.class, server.url("/").toString()); From bfa6d7e5a29b2fdbee59c80d50079e498fa97128 Mon Sep 17 00:00:00 2001 From: mroccyen Date: Fri, 25 Mar 2022 19:44:44 +0800 Subject: [PATCH 26/29] add some example --- core/src/main/java/feign/stream/StreamDecoder.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java index 827d9a132..562bf2ad2 100644 --- a/core/src/main/java/feign/stream/StreamDecoder.java +++ b/core/src/main/java/feign/stream/StreamDecoder.java @@ -38,6 +38,11 @@ * .decoder(StreamDecoder.create(JacksonIteratorDecoder.create())) * .doNotCloseAfterDecode() // Required for streaming * .target(GitHub.class, "https://api.github.com"); + * or + * Feign.builder() + * .decoder(StreamDecoder.create(JacksonIteratorDecoder.create(), (r, t) -> "hello world"))) + * .doNotCloseAfterDecode() // Required for streaming + * .target(GitHub.class, "https://api.github.com"); * interface GitHub { * {@literal @}RequestLine("GET /repos/{owner}/{repo}/contributors") * Stream contributors(@Param("owner") String owner, @Param("repo") String repo); From 0a400d32b5b50b971a223a90ccc560e501d50871 Mon Sep 17 00:00:00 2001 From: mroccyen Date: Sun, 27 Mar 2022 22:08:52 +0800 Subject: [PATCH 27/29] Optimize StreamDecoder --- core/src/main/java/feign/stream/StreamDecoder.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java index 562bf2ad2..d6b958d19 100644 --- a/core/src/main/java/feign/stream/StreamDecoder.java +++ b/core/src/main/java/feign/stream/StreamDecoder.java @@ -21,6 +21,7 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Iterator; +import java.util.Optional; import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -52,22 +53,24 @@ public final class StreamDecoder implements Decoder { private final Decoder iteratorDecoder; - private final Decoder delegateDecoder; + private final Optional delegateDecoder; StreamDecoder(Decoder iteratorDecoder, Decoder delegateDecoder) { this.iteratorDecoder = iteratorDecoder; - this.delegateDecoder = delegateDecoder; + this.delegateDecoder = delegateDecoder != null + ? Optional.of(delegateDecoder) + : Optional.empty(); } @Override public Object decode(Response response, Type type) throws IOException, FeignException { if (!isStream(type)) { - if (delegateDecoder == null) { + if (!delegateDecoder.isPresent()) { throw new IllegalArgumentException("StreamDecoder supports types other than stream. " + "When type is not stream, the delegate decoder needs to be setting."); } else { - return delegateDecoder.decode(response, type); + return delegateDecoder.get().decode(response, type); } } ParameterizedType streamType = (ParameterizedType) type; From d0f722ee0bd37f68c053a015417fe1e8facedb09 Mon Sep 17 00:00:00 2001 From: mroccyen Date: Sun, 27 Mar 2022 22:39:29 +0800 Subject: [PATCH 28/29] add a section of README for stream decoder --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 7166770eb..501a40e04 100644 --- a/README.md +++ b/README.md @@ -568,6 +568,39 @@ public class Example { } ``` +If any methods in your interface return type `Stream`, you'll need to configure a `StreamDecoder`. + +Here's how to configure Stream decoder without delegate decoder: + +```java +public class Example { + public static void main(String[] args) { + GitHub github = Feign.builder() + .decoder(StreamDecoder.create((r, t) -> { + BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + })) + .target(GitHub.class, "https://api.github.com"); + } +} +``` + +Here's how to configure Stream decoder with delegate decoder: + +```java + +public class Example { + public static void main(String[] args) { + GitHub github = Feign.builder() + .decoder(StreamDecoder.create((r, t) -> { + BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8)); + return bufferedReader.lines().iterator(); + }, (r, t) -> "this is delegate decoder")) + .target(GitHub.class, "https://api.github.com"); + } +} +``` + ### Encoders The simplest way to send a request body to a server is to define a `POST` method that has a `String` or `byte[]` parameter without any annotations on it. You will likely need to add a `Content-Type` header. From 55e98325a532a7844abd20726168048b5443483f Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Sat, 30 Apr 2022 01:46:05 +1200 Subject: [PATCH 29/29] Update StreamDecoder.java --- core/src/main/java/feign/stream/StreamDecoder.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java index d6b958d19..e631360d6 100644 --- a/core/src/main/java/feign/stream/StreamDecoder.java +++ b/core/src/main/java/feign/stream/StreamDecoder.java @@ -57,9 +57,7 @@ public final class StreamDecoder implements Decoder { StreamDecoder(Decoder iteratorDecoder, Decoder delegateDecoder) { this.iteratorDecoder = iteratorDecoder; - this.delegateDecoder = delegateDecoder != null - ? Optional.of(delegateDecoder) - : Optional.empty(); + this.delegateDecoder = Optional.ofNullable(delegateDecoder); } @Override