diff --git a/README.md b/README.md index 6a54f0baa..cb6b97a4f 100644 --- a/README.md +++ b/README.md @@ -474,6 +474,23 @@ public class Example { NB: you may also need to add `SOAPErrorDecoder` if SOAP Faults are returned in response with error http codes (4xx, 5xx, ...) +#### Fastjson2 + +[fastjson2](./fastjson2) includes an encoder and decoder you can use with a JSON API. + +Add `Fastjson2Encoder` and/or `Fastjson2Decoder` to your `Feign.Builder` like so: + +```java +public class Example { + public static void main(String[] args) { + GitHub github = Feign.builder() + .encoder(new Fastjson2Encoder()) + .decoder(new Fastjson2Decoder()) + .target(GitHub.class, "https://api.github.com"); + } +} +``` + ### Contract #### JAX-RS diff --git a/fastjson2/README.md b/fastjson2/README.md new file mode 100644 index 000000000..8d283ea30 --- /dev/null +++ b/fastjson2/README.md @@ -0,0 +1,22 @@ +Fastjson2 Codec +=================== + +This module adds support for encoding and decoding JSON via Fastjson2. + +Add `Fastjson2Encoder` and/or `Fastjson2Decoder` to your `Feign.Builder` like so: + +```java +GitHub github = Feign.builder() + .encoder(new Fastjson2Encoder()) + .decoder(new Fastjson2Decoder()) + .target(GitHub.class, "https://api.github.com"); +``` + +If you want to customize, provide it to the `Fastjson2Encoder` and `Fastjson2Decoder`: + +```java +GitHub github = Feign.builder() + .encoder(new Fastjson2Encoder(new JSONWriter.Feature[]{JSONWriter.Feature.WriteNonStringValueAsString}) + .decoder(new Fastjson2Decoder(new JSONReader.Feature[]{JSONReader.Feature.EmptyStringAsNull})) + .target(GitHub.class, "https://api.github.com"); +``` diff --git a/fastjson2/pom.xml b/fastjson2/pom.xml new file mode 100644 index 000000000..3000562f7 --- /dev/null +++ b/fastjson2/pom.xml @@ -0,0 +1,52 @@ + + + + 4.0.0 + + + io.github.openfeign + parent + 13.3-SNAPSHOT + + + feign-fastjson2 + Feign Fastjson2 + Feign Fastjson2 + + + ${project.basedir}/.. + + + + + ${project.groupId} + feign-core + + + + com.alibaba.fastjson2 + fastjson2 + + + + ${project.groupId} + feign-core + test-jar + test + + + diff --git a/fastjson2/src/main/java/feign/fastjson2/Fastjson2Decoder.java b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Decoder.java new file mode 100644 index 000000000..ce9c28786 --- /dev/null +++ b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Decoder.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2024 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.fastjson2; + +import static feign.Util.ensureClosed; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.JSONReader; +import feign.FeignException; +import feign.Response; +import feign.Util; +import feign.codec.Decoder; +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Type; + +/** + * @author changjin wei(魏昌进) + */ +public class Fastjson2Decoder implements Decoder { + + private final JSONReader.Feature[] features; + + public Fastjson2Decoder() { + this(new JSONReader.Feature[0]); + } + + public Fastjson2Decoder(JSONReader.Feature[] features) { + this.features = features; + } + + @Override + public Object decode(Response response, Type type) throws IOException, FeignException { + if (response.status() == 404 || response.status() == 204) return Util.emptyValueOf(type); + if (response.body() == null) return null; + Reader reader = response.body().asReader(response.charset()); + try { + return JSON.parseObject(reader, type, features); + } catch (JSONException e) { + if (e.getCause() != null && e.getCause() instanceof IOException) { + throw IOException.class.cast(e.getCause()); + } + throw e; + } finally { + ensureClosed(reader); + } + } +} diff --git a/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java new file mode 100644 index 000000000..2353fe394 --- /dev/null +++ b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2024 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.fastjson2; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONWriter; +import feign.RequestTemplate; +import feign.Util; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import java.lang.reflect.Type; + +/** + * @author changjin wei(魏昌进) + */ +public class Fastjson2Encoder implements Encoder { + + private final JSONWriter.Feature[] features; + + public Fastjson2Encoder() { + this(new JSONWriter.Feature[0]); + } + + public Fastjson2Encoder(JSONWriter.Feature[] features) { + this.features = features; + } + + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { + template.body(JSON.toJSONBytes(object, features), Util.UTF_8); + } +} diff --git a/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java b/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java new file mode 100644 index 000000000..e99fe0539 --- /dev/null +++ b/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java @@ -0,0 +1,211 @@ +/* + * Copyright 2012-2024 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.fastjson2; + +import static feign.Util.UTF_8; +import static feign.assertj.FeignAssertions.assertThat; + +import com.alibaba.fastjson2.TypeReference; +import feign.Request; +import feign.RequestTemplate; +import feign.Response; +import feign.Util; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * @author changjin wei(魏昌进) + */ +@SuppressWarnings("deprecation") +class FastJsonCodecTest { + + private String zonesJson = + "" // + + "[" + + System.lineSeparator() // + + " {" + + System.lineSeparator() // + + " \"name\": \"denominator.io.\"" + + System.lineSeparator() // + + " }," + + System.lineSeparator() // + + " {" + + System.lineSeparator() // + + " \"name\": \"denominator.io.\"," + + System.lineSeparator() // + + " \"id\": \"ABCD\"" + + System.lineSeparator() // + + " }" + + System.lineSeparator() // + + "]" + + System.lineSeparator(); + + @Test + void encodesMapObjectNumericalValuesAsInteger() { + Map map = new LinkedHashMap<>(); + map.put("foo", 1); + + RequestTemplate template = new RequestTemplate(); + new Fastjson2Encoder().encode(map, map.getClass(), template); + + assertThat(template) + .hasBody( + "" // + + "{" // + + "\"foo\":1" // + + "}"); + } + + @Test + void encodesFormParams() { + Map form = new LinkedHashMap<>(); + form.put("foo", 1); + form.put("bar", Arrays.asList(2, 3)); + + RequestTemplate template = new RequestTemplate(); + new Fastjson2Encoder().encode(form, new TypeReference>() {}.getType(), template); + + assertThat(template) + .hasBody( + "" // + + "{" // + + "\"foo\":1," // + + "\"bar\":[2,3]" // + + "}"); + } + + @Test + void decodes() throws Exception { + List zones = new LinkedList<>(); + zones.add(new Zone("denominator.io.")); + zones.add(new Zone("denominator.io.", "ABCD")); + + Response response = + Response.builder() + .status(200) + .reason("OK") + .request( + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .body(zonesJson, UTF_8) + .build(); + assertThat( + new Fastjson2Decoder().decode(response, new TypeReference>() {}.getType())) + .isEqualTo(zones); + } + + @Test + void nullBodyDecodesToNull() throws Exception { + Response response = + Response.builder() + .status(204) + .reason("OK") + .request( + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .build(); + assertThat(new Fastjson2Decoder().decode(response, String.class)).isNull(); + } + + @Test + void emptyBodyDecodesToNull() throws Exception { + Response response = + Response.builder() + .status(204) + .reason("OK") + .request( + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .body(new byte[0]) + .build(); + assertThat(new Fastjson2Decoder().decode(response, String.class)).isNull(); + } + + @Test + void decoderCharset() throws IOException { + Zone zone = new Zone("denominator.io.", "ÁÉÍÓÚÀÈÌÒÙÄËÏÖÜÑ"); + + Map> headers = new HashMap<>(); + headers.put("Content-Type", Arrays.asList("application/json;charset=ISO-8859-1")); + + Response response = + Response.builder() + .status(200) + .reason("OK") + .request( + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(headers) + .body( + new String( + "" // + + "{" + + " \"name\" : \"DENOMINATOR.IO.\"," + + " \"id\" : \"ÁÉÍÓÚÀÈÌÒÙÄËÏÖÜÑ\"" + + "}") + .getBytes(StandardCharsets.ISO_8859_1)) + .build(); + assertThat( + ((Zone) + new Fastjson2Decoder().decode(response, new TypeReference() {}.getType()))) + .containsEntry("id", zone.get("id")); + } + + static class Zone extends LinkedHashMap { + + private static final long serialVersionUID = 1L; + + Zone() { + // for reflective instantiation. + } + + Zone(String name) { + this(name, null); + } + + Zone(String name, String id) { + put("name", name); + if (id != null) { + put("id", id); + } + } + } + + /** Enabled via {@link feign.Feign.Builder#dismiss404()} */ + @Test + void notFoundDecodesToEmpty() throws Exception { + Response response = + Response.builder() + .status(404) + .reason("NOT FOUND") + .request( + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .build(); + assertThat((byte[]) new Fastjson2Decoder().decode(response, byte[].class)).isEmpty(); + } +} diff --git a/fastjson2/src/test/java/feign/fastjson2/examples/GitHubExample.java b/fastjson2/src/test/java/feign/fastjson2/examples/GitHubExample.java new file mode 100644 index 000000000..2ce18b7cd --- /dev/null +++ b/fastjson2/src/test/java/feign/fastjson2/examples/GitHubExample.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2024 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.fastjson2.examples; + +import feign.Feign; +import feign.Param; +import feign.RequestLine; +import feign.fastjson2.Fastjson2Decoder; +import java.util.List; + +/** adapted from {@code com.example.retrofit.GitHubClient} */ +public class GitHubExample { + + public static void main(String... args) { + GitHub github = + Feign.builder() + .decoder(new Fastjson2Decoder()) + .target(GitHub.class, "https://api.github.com"); + + System.out.println("Let's fetch and print a list of the contributors to this library."); + List contributors = github.contributors("netflix", "feign"); + for (Contributor contributor : contributors) { + System.out.println(contributor.login + " (" + contributor.contributions + ")"); + } + } + + interface GitHub { + + @RequestLine("GET /repos/{owner}/{repo}/contributors") + List contributors(@Param("owner") String owner, @Param("repo") String repo); + } + + static class Contributor { + + private String login; + private int contributions; + + void setLogin(String login) { + this.login = login; + } + + void setContributions(int contributions) { + this.contributions = contributions; + } + } +} diff --git a/pom.xml b/pom.xml index e0a8c2970..eb1bd3f1b 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,7 @@ example-wikipedia-with-springboot benchmark moshi + fastjson2 @@ -96,6 +97,7 @@ 2.17.0 3.25.3 5.11.0 + 2.0.48 3.13.0 3.1.1 @@ -410,6 +412,12 @@ ${slf4j.version} + + com.alibaba.fastjson2 + fastjson2 + ${fastjson2.version} + + diff --git a/src/docs/overview-mindmap.iuml b/src/docs/overview-mindmap.iuml index 50db9818e..ee3b26633 100644 --- a/src/docs/overview-mindmap.iuml +++ b/src/docs/overview-mindmap.iuml @@ -35,6 +35,7 @@ left side *** Sax *** JSON-java *** Moshi +*** Fastjson2 ** metrics *** Dropwizard Metrics 5 *** Micrometer