From 6a2ca6308435552ad0f28a1058490db586de25f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B2=BB=E4=BF=9D?= Date: Mon, 21 Oct 2024 10:13:23 +0800 Subject: [PATCH] jax-rs support (#3110) * jax-rs support * fix error: reference not found --- .../extension-jaxrs-jakarta/pom.xml | 55 ++++ .../jakarta/FastJson2AutoDiscoverable.java | 36 +++ .../jaxrs/jakarta/FastJson2Feature.java | 60 ++++ .../jaxrs/jakarta/FastJson2Provider.java | 265 +++++++++++++++++ .../jakarta.ws.rs.ext.MessageBodyReader | 1 + .../jakarta.ws.rs.ext.MessageBodyWriter | 1 + .../services/jakarta.ws.rs.ext.Providers | 1 + ...sfish.jersey.internal.spi.AutoDiscoverable | 1 + .../jaxrs/jakarta/FastJson2ProviderTest.java | 84 ++++++ .../support/jaxrs/jakarta/JaxRsResource.java | 33 +++ .../support/jaxrs/jakarta/model/User.java | 19 ++ extension-jaxrs/extension-jaxrs-javax/pom.xml | 45 +++ .../javax/FastJson2AutoDiscoverable.java | 37 +++ .../support/jaxrs/javax/FastJson2Feature.java | 61 ++++ .../jaxrs/javax/FastJson2Provider.java | 267 ++++++++++++++++++ .../javax.ws.rs.ext.MessageBodyReader | 1 + .../javax.ws.rs.ext.MessageBodyWriter | 1 + .../services/javax.ws.rs.ext.Providers | 1 + ...sfish.jersey.internal.spi.AutoDiscoverable | 1 + .../jaxrs/javax/FastJson2ProviderTest.java | 83 ++++++ .../support/jaxrs/javax/JaxRsResource.java | 33 +++ .../support/jaxrs/javax/model/User.java | 19 ++ extension-jaxrs/pom.xml | 40 +++ pom.xml | 1 + 24 files changed, 1146 insertions(+) create mode 100644 extension-jaxrs/extension-jaxrs-jakarta/pom.xml create mode 100644 extension-jaxrs/extension-jaxrs-jakarta/src/main/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2AutoDiscoverable.java create mode 100644 extension-jaxrs/extension-jaxrs-jakarta/src/main/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2Feature.java create mode 100644 extension-jaxrs/extension-jaxrs-jakarta/src/main/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2Provider.java create mode 100644 extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/jakarta.ws.rs.ext.MessageBodyReader create mode 100644 extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/jakarta.ws.rs.ext.MessageBodyWriter create mode 100644 extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/jakarta.ws.rs.ext.Providers create mode 100644 extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable create mode 100644 extension-jaxrs/extension-jaxrs-jakarta/src/test/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2ProviderTest.java create mode 100644 extension-jaxrs/extension-jaxrs-jakarta/src/test/java/com/alibaba/fastjson2/support/jaxrs/jakarta/JaxRsResource.java create mode 100644 extension-jaxrs/extension-jaxrs-jakarta/src/test/java/com/alibaba/fastjson2/support/jaxrs/jakarta/model/User.java create mode 100644 extension-jaxrs/extension-jaxrs-javax/pom.xml create mode 100644 extension-jaxrs/extension-jaxrs-javax/src/main/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2AutoDiscoverable.java create mode 100644 extension-jaxrs/extension-jaxrs-javax/src/main/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2Feature.java create mode 100644 extension-jaxrs/extension-jaxrs-javax/src/main/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2Provider.java create mode 100644 extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyReader create mode 100644 extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyWriter create mode 100644 extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers create mode 100644 extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable create mode 100644 extension-jaxrs/extension-jaxrs-javax/src/test/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2ProviderTest.java create mode 100644 extension-jaxrs/extension-jaxrs-javax/src/test/java/com/alibaba/fastjson2/support/jaxrs/javax/JaxRsResource.java create mode 100644 extension-jaxrs/extension-jaxrs-javax/src/test/java/com/alibaba/fastjson2/support/jaxrs/javax/model/User.java create mode 100644 extension-jaxrs/pom.xml diff --git a/extension-jaxrs/extension-jaxrs-jakarta/pom.xml b/extension-jaxrs/extension-jaxrs-jakarta/pom.xml new file mode 100644 index 0000000000..5ce3beb7e4 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-jakarta/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + com.alibaba.fastjson2 + fastjson2-extension-jaxrs + 2.0.54-SNAPSHOT + + + fastjson2-extension-jaxrs-jakarta + + + 11 + 11 + ${maven.compiler.source} + + 3.1.0 + + + + + org.glassfish.jersey.core + jersey-common + ${jersey.version} + true + + + jakarta.ws.rs + jakarta.ws.rs-api + ${jakarta.ws.rs.version} + provided + + + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey.version} + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-jdk-http + ${jersey.version} + test + + + org.skyscreamer + jsonassert + 1.5.3 + test + + + diff --git a/extension-jaxrs/extension-jaxrs-jakarta/src/main/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2AutoDiscoverable.java b/extension-jaxrs/extension-jaxrs-jakarta/src/main/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2AutoDiscoverable.java new file mode 100644 index 0000000000..145e73c333 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-jakarta/src/main/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2AutoDiscoverable.java @@ -0,0 +1,36 @@ +package com.alibaba.fastjson2.support.jaxrs.jakarta; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.FeatureContext; +import org.glassfish.jersey.internal.spi.AutoDiscoverable; + +/** + * FastJson2AutoDiscoverable + * 参考: com.alibaba.fastjson.support.jaxrs.FastJsonAutoDiscoverable + * + * @author 张治保 + * @since 2024/10/16 + * @see AutoDiscoverable + */ +@Priority(AutoDiscoverable.DEFAULT_PRIORITY - 1) +public class FastJson2AutoDiscoverable + implements AutoDiscoverable { + public static final String FASTJSON_AUTO_DISCOVERABLE = "fastjson.auto.discoverable"; + public static volatile boolean autoDiscover; + + static { + autoDiscover = Boolean.parseBoolean( + System.getProperty(FASTJSON_AUTO_DISCOVERABLE, Boolean.TRUE.toString())); + } + + @Override + public void configure(final FeatureContext context) { + final Configuration config = context.getConfiguration(); + + // Register FastJson. + if (!config.isRegistered(FastJson2Feature.class) && autoDiscover) { + context.register(FastJson2Feature.class); + } + } +} diff --git a/extension-jaxrs/extension-jaxrs-jakarta/src/main/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2Feature.java b/extension-jaxrs/extension-jaxrs-jakarta/src/main/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2Feature.java new file mode 100644 index 0000000000..f6fc6a5305 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-jakarta/src/main/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2Feature.java @@ -0,0 +1,60 @@ +package com.alibaba.fastjson2.support.jaxrs.jakarta; + +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Feature; +import jakarta.ws.rs.core.FeatureContext; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.MessageBodyWriter; +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.internal.InternalProperties; +import org.glassfish.jersey.internal.util.PropertiesHelper; +/** + * FastJson2Feature + * 参考:com.alibaba.fastjson.support.jaxrs.FastJsonFeature + * + * @author 张治保 + * @since 2024/10/16 + * @see Feature + */ +public class FastJson2Feature + implements Feature { + private static final String JSON_FEATURE = FastJson2Feature.class.getSimpleName(); + + @Override + public boolean configure(final FeatureContext context) { + try { + final Configuration config = context.getConfiguration(); + + final String jsonFeature = CommonProperties.getValue( + config.getProperties(), + config.getRuntimeType(), + InternalProperties.JSON_FEATURE, + JSON_FEATURE, + String.class + ); + + // Other JSON providers registered. + if (!JSON_FEATURE.equalsIgnoreCase(jsonFeature)) { + return false; + } + + // Disable other JSON providers. + context.property( + PropertiesHelper.getPropertyNameForRuntime( + InternalProperties.JSON_FEATURE, + config.getRuntimeType() + ), + JSON_FEATURE + ); + + // Register FastJson. + if (!config.isRegistered(FastJson2Provider.class)) { + context.register(FastJson2Provider.class, MessageBodyReader.class, MessageBodyWriter.class); + } + } catch (NoSuchMethodError e) { + // skip + } + + return true; + } +} diff --git a/extension-jaxrs/extension-jaxrs-jakarta/src/main/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2Provider.java b/extension-jaxrs/extension-jaxrs-jakarta/src/main/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2Provider.java new file mode 100644 index 0000000000..1c921ee983 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-jakarta/src/main/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2Provider.java @@ -0,0 +1,265 @@ +package com.alibaba.fastjson2.support.jaxrs.jakarta; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.support.config.FastJsonConfig; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.*; +import jakarta.ws.rs.ext.*; +import lombok.Getter; +import lombok.Setter; + +import java.io.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +/** + * Fastjson for JAX-RS Provider. + * 参考:com.alibaba.fastjson.support.jaxrs.FastJsonProvider + * @author 张治保 + * @since 2024/10/16 + * @see MessageBodyReader + * @see MessageBodyWriter + */ + +@Provider +@Consumes(MediaType.WILDCARD) +@Produces(MediaType.WILDCARD) +public class FastJson2Provider + implements MessageBodyReader, MessageBodyWriter { + /** + * These are classes that we never use for reading + * (never try to deserialize instances of these types). + */ + public static final Class[] DEFAULT_UNREADABLES = new Class[]{ + InputStream.class, Reader.class + }; + + /** + * These are classes that we never use for writing + * (never try to serialize instances of these types). + */ + public static final Class[] DEFAULT_UNWRITABLES = new Class[]{ + InputStream.class, + OutputStream.class, Writer.class, + StreamingOutput.class, Response.class + }; + + /** + * Injectable context object used to locate configured + * instance of {@link FastJsonConfig} to use for actual + * serialization. + */ + @Context + protected Providers providers; + + /** + * with fastJson config + */ + @Getter + @Setter + private FastJsonConfig fastJsonConfig = new FastJsonConfig(); + + /** + * allow serialize/deserialize types in clazzes + */ + private Class[] clazzes; + + /** + * Can serialize/deserialize all types. + */ + public FastJson2Provider() { + this((Class[]) null); + } + + /** + * Only serialize/deserialize all types in clazzes. + */ + public FastJson2Provider(Class[] clazzes) { + this.clazzes = clazzes; + } + + /** + * Check some are interface/abstract classes to exclude. + * + * @param type the type + * @param classes the classes + * @return the boolean + */ + protected boolean isNotAssignableFrom(Class type, Class[] classes) { + if (type == null) { + return true; + } + // there are some other abstract/interface types to exclude too: + for (Class cls : classes) { + if (cls.isAssignableFrom(type)) { + return true; + } + } + return false; + } + + /** + * Check whether a class can be serialized or deserialized. It can check + * based on packages, annotations on entities or explicit classes. + * + * @param type class need to check + * @return true if valid + */ + protected boolean isValidType(Class type, Annotation[] classAnnotations) { + if (type == null) { + return false; + } + if (clazzes != null) { + for (Class cls : clazzes) { + // must strictly equal. Don't check inheritance + if (cls == type) { + return true; + } + } + return false; + } + return true; + } + + /** + * Check media type like "application/json". + * + * @param mediaType media type + * @return true if the media type is valid + */ + protected boolean hasNotMatchingMediaType(MediaType mediaType) { + if (mediaType == null) { + return false; + } + String subtype = mediaType.getSubtype(); + return !(("json".equalsIgnoreCase(subtype)) + || (subtype.endsWith("+json")) + || ("javascript".equals(subtype)) + || ("x-javascript".equals(subtype)) + || ("x-json".equals(subtype)) + || ("x-www-form-urlencoded".equalsIgnoreCase(subtype)) + || (subtype.endsWith("x-www-form-urlencoded"))); + } + + /** + * Method that JAX-RS container calls to try to check whether given value + * (of specified type) can be serialized by this provider. + */ + @Override + public boolean isWriteable( + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType) { + if (hasNotMatchingMediaType(mediaType)) { + return false; + } + if (isNotAssignableFrom(type, DEFAULT_UNWRITABLES)) { + return false; + } + return isValidType(type, annotations); + } + + /** + * Method that JAX-RS container calls to serialize given value. + */ + @Override + public void writeTo( + Object object, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream + ) throws IOException, WebApplicationException { + FastJsonConfig config = locateConfigProvider(type, mediaType); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + if (object instanceof String && JSON.isValidObject((String) object)) { + byte[] strBytes = ((String) object).getBytes(config.getCharset()); + baos.write(strBytes, 0, strBytes.length); + } else if (object instanceof byte[] && JSON.isValid((byte[]) object)) { + byte[] strBytes = (byte[]) object; + baos.write(strBytes, 0, strBytes.length); + } else { + JSON.writeTo( + baos, + object, + config.getDateFormat(), + config.getWriterFilters(), + config.getWriterFeatures() + ); + } + baos.writeTo(entityStream); + } + } + + /** + * Method that JAX-RS container calls to try to check whether values of + * given type (and media type) can be deserialized by this provider. + */ + @Override + public boolean isReadable( + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType) { + if (hasNotMatchingMediaType(mediaType)) { + return false; + } + if (isNotAssignableFrom(type, DEFAULT_UNREADABLES)) { + return false; + } + return isValidType(type, annotations); + } + + /** + * Method that JAX-RS container calls to deserialize given value. + */ + @Override + public Object readFrom( + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + InputStream entityStream + ) throws IOException, WebApplicationException { + FastJsonConfig config = locateConfigProvider(type, mediaType); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buf = new byte[1024 * 64]; + for (; ; ) { + int len = entityStream.read(buf); + if (len == -1) { + break; + } + if (len > 0) { + baos.write(buf, 0, len); + } + } + byte[] bytes = baos.toByteArray(); + return JSON.parseObject(bytes, genericType, config.getDateFormat(), config.getReaderFilters(), config.getReaderFeatures()); + } catch (JSONException ex) { + throw new WebApplicationException(ex); + } + } + + /** + * Helper method that is called if no config has been explicitly configured. + */ + protected FastJsonConfig locateConfigProvider(Class type, MediaType mediaType) { + if (providers != null) { + ContextResolver resolver = providers.getContextResolver(FastJsonConfig.class, mediaType); + if (resolver == null) { + resolver = providers.getContextResolver(FastJsonConfig.class, null); + } + if (resolver != null) { + return resolver.getContext(type); + } + } + return fastJsonConfig; + } +} diff --git a/extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/jakarta.ws.rs.ext.MessageBodyReader b/extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/jakarta.ws.rs.ext.MessageBodyReader new file mode 100644 index 0000000000..66167e3646 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/jakarta.ws.rs.ext.MessageBodyReader @@ -0,0 +1 @@ +com.alibaba.fastjson2.support.jaxrs.jakarta.FastJson2Provider diff --git a/extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/jakarta.ws.rs.ext.MessageBodyWriter b/extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/jakarta.ws.rs.ext.MessageBodyWriter new file mode 100644 index 0000000000..66167e3646 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/jakarta.ws.rs.ext.MessageBodyWriter @@ -0,0 +1 @@ +com.alibaba.fastjson2.support.jaxrs.jakarta.FastJson2Provider diff --git a/extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/jakarta.ws.rs.ext.Providers b/extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/jakarta.ws.rs.ext.Providers new file mode 100644 index 0000000000..66167e3646 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/jakarta.ws.rs.ext.Providers @@ -0,0 +1 @@ +com.alibaba.fastjson2.support.jaxrs.jakarta.FastJson2Provider diff --git a/extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable b/extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable new file mode 100644 index 0000000000..49da5a0a60 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-jakarta/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable @@ -0,0 +1 @@ +com.alibaba.fastjson2.support.jaxrs.jakarta.FastJson2AutoDiscoverable diff --git a/extension-jaxrs/extension-jaxrs-jakarta/src/test/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2ProviderTest.java b/extension-jaxrs/extension-jaxrs-jakarta/src/test/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2ProviderTest.java new file mode 100644 index 0000000000..b5ae0afaed --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-jakarta/src/test/java/com/alibaba/fastjson2/support/jaxrs/jakarta/FastJson2ProviderTest.java @@ -0,0 +1,84 @@ +package com.alibaba.fastjson2.support.jaxrs.jakarta; + +import com.alibaba.fastjson2.support.jaxrs.jakarta.model.User; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.MessageBodyWriter; +import lombok.SneakyThrows; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author 张治保 + * @since 2024/10/16 + */ +public class FastJson2ProviderTest + extends JerseyTest { + private static final FastJson2Provider fastJson2Provider = new FastJson2Provider(); + + @Override + protected Application configure() { + enable(TestProperties.LOG_TRAFFIC); + enable(TestProperties.DUMP_ENTITY); + return new ResourceConfig(JaxRsResource.class) + .register(fastJson2Provider, MessageBodyReader.class, MessageBodyWriter.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(fastJson2Provider, MessageBodyReader.class, MessageBodyWriter.class); + } + + @Test + @SneakyThrows + void testGet() { + String json = successAndGet( + target().path("/test/get") + .request() + .get() + ); + JSONAssert.assertEquals("{\"name\":\"fastjson2\",\"age\":0}", json, true); + } + + @SneakyThrows + @Test + void testGetPath() { + //MODULE_ORDER: One does not simply declare modules! + String json = successAndGet( + target().path("/test/get/hello-fastjson2") + .request() + .get() + ); + JSONAssert.assertEquals("{\"name\":\"hello-fastjson2\",\"age\":0}", json, true); + } + + @Test + @SneakyThrows + void testPost() { + User user = new User() + .setName("fastjson2") + .setAge(1); + String json = successAndGet( + target().path("/test/post") + .request(MediaType.APPLICATION_JSON) + .post(Entity.json(user)) + ); + JSONAssert.assertEquals("{\"name\":\"fastjson2\",\"age\":1}", json, true); + } + + public static String successAndGet(Response response) { + try (response) { + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + return response.readEntity(String.class); + } + } +} diff --git a/extension-jaxrs/extension-jaxrs-jakarta/src/test/java/com/alibaba/fastjson2/support/jaxrs/jakarta/JaxRsResource.java b/extension-jaxrs/extension-jaxrs-jakarta/src/test/java/com/alibaba/fastjson2/support/jaxrs/jakarta/JaxRsResource.java new file mode 100644 index 0000000000..db8e612641 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-jakarta/src/test/java/com/alibaba/fastjson2/support/jaxrs/jakarta/JaxRsResource.java @@ -0,0 +1,33 @@ +package com.alibaba.fastjson2.support.jaxrs.jakarta; + +import com.alibaba.fastjson2.support.jaxrs.jakarta.model.User; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +/** + * @author 张治保 + * @since 2024/10/16 + */ +@Path("/test") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class JaxRsResource { + @GET + @Path("/get") + public User get() { + return new User().setName("fastjson2"); + } + + @GET + @Path("/get/{name}") + public User getPath(@PathParam("name") String name) { + return new User().setName(name); + } + + @POST + @Path("/post") + public User post(User user) { + return user.setAge(user.getAge()) + .setName(user.getName()); + } +} diff --git a/extension-jaxrs/extension-jaxrs-jakarta/src/test/java/com/alibaba/fastjson2/support/jaxrs/jakarta/model/User.java b/extension-jaxrs/extension-jaxrs-jakarta/src/test/java/com/alibaba/fastjson2/support/jaxrs/jakarta/model/User.java new file mode 100644 index 0000000000..7f6ade88ed --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-jakarta/src/test/java/com/alibaba/fastjson2/support/jaxrs/jakarta/model/User.java @@ -0,0 +1,19 @@ +package com.alibaba.fastjson2.support.jaxrs.jakarta.model; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * @author 张治保 + * @since 2024/10/16 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class User { + private String name; + private int age; +} diff --git a/extension-jaxrs/extension-jaxrs-javax/pom.xml b/extension-jaxrs/extension-jaxrs-javax/pom.xml new file mode 100644 index 0000000000..bef2994c96 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-javax/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + com.alibaba.fastjson2 + fastjson2-extension-jaxrs + 2.0.54-SNAPSHOT + + + fastjson2-extension-jaxrs-javax + + + + org.glassfish.jersey.core + jersey-common + true + + + javax.ws.rs + javax.ws.rs-api + ${javax.ws.rs.version} + provided + + + + org.glassfish.jersey.inject + jersey-hk2 + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-jdk-http + test + + + org.skyscreamer + jsonassert + 1.5.3 + test + + + + diff --git a/extension-jaxrs/extension-jaxrs-javax/src/main/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2AutoDiscoverable.java b/extension-jaxrs/extension-jaxrs-javax/src/main/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2AutoDiscoverable.java new file mode 100644 index 0000000000..fd328785be --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-javax/src/main/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2AutoDiscoverable.java @@ -0,0 +1,37 @@ +package com.alibaba.fastjson2.support.jaxrs.javax; + +import org.glassfish.jersey.internal.spi.AutoDiscoverable; + +import javax.annotation.Priority; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.FeatureContext; + +/** + * FastJson2AutoDiscoverable + * 参考: com.alibaba.fastjson.support.jaxrs.FastJsonAutoDiscoverable + * + * @author 张治保 + * @since 2024/10/16 + * @see AutoDiscoverable + */ +@Priority(AutoDiscoverable.DEFAULT_PRIORITY - 1) +public class FastJson2AutoDiscoverable + implements AutoDiscoverable { + public static final String FASTJSON_AUTO_DISCOVERABLE = "fastjson.auto.discoverable"; + public static volatile boolean autoDiscover; + + static { + autoDiscover = Boolean.parseBoolean( + System.getProperty(FASTJSON_AUTO_DISCOVERABLE, Boolean.TRUE.toString())); + } + + @Override + public void configure(final FeatureContext context) { + final Configuration config = context.getConfiguration(); + + // Register FastJson. + if (!config.isRegistered(FastJson2Feature.class) && autoDiscover) { + context.register(FastJson2Feature.class); + } + } +} diff --git a/extension-jaxrs/extension-jaxrs-javax/src/main/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2Feature.java b/extension-jaxrs/extension-jaxrs-javax/src/main/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2Feature.java new file mode 100644 index 0000000000..303a7ee600 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-javax/src/main/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2Feature.java @@ -0,0 +1,61 @@ +package com.alibaba.fastjson2.support.jaxrs.javax; + +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.internal.InternalProperties; +import org.glassfish.jersey.internal.util.PropertiesHelper; + +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +/** + * FastJson2Feature + * 参考:@see com.alibaba.fastjson.support.jaxrs.FastJsonFeature + * + * @author 张治保 + * @since 2024/10/16 + * @see Feature + */ +public class FastJson2Feature + implements Feature { + private static final String JSON_FEATURE = FastJson2Feature.class.getSimpleName(); + + @Override + public boolean configure(final FeatureContext context) { + try { + final Configuration config = context.getConfiguration(); + + final String jsonFeature = CommonProperties.getValue( + config.getProperties(), + config.getRuntimeType(), + InternalProperties.JSON_FEATURE, + JSON_FEATURE, + String.class + ); + + // Other JSON providers registered. + if (!JSON_FEATURE.equalsIgnoreCase(jsonFeature)) { + return false; + } + + // Disable other JSON providers. + context.property( + PropertiesHelper.getPropertyNameForRuntime( + InternalProperties.JSON_FEATURE, + config.getRuntimeType() + ), + JSON_FEATURE + ); + + // Register FastJson. + if (!config.isRegistered(FastJson2Provider.class)) { + context.register(FastJson2Provider.class, MessageBodyReader.class, MessageBodyWriter.class); + } + } catch (NoSuchMethodError e) { + // skip + } + + return true; + } +} diff --git a/extension-jaxrs/extension-jaxrs-javax/src/main/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2Provider.java b/extension-jaxrs/extension-jaxrs-javax/src/main/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2Provider.java new file mode 100644 index 0000000000..8ca7fba943 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-javax/src/main/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2Provider.java @@ -0,0 +1,267 @@ +package com.alibaba.fastjson2.support.jaxrs.javax; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.support.config.FastJsonConfig; +import lombok.Getter; +import lombok.Setter; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.*; +import javax.ws.rs.ext.*; + +import java.io.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +/** + * Fastjson for JAX-RS Provider. + * 参考:com.alibaba.fastjson.support.jaxrs.FastJsonProvider + * + * @author 张治保 + * @since 2024/10/16 + * @see MessageBodyReader + * @see MessageBodyWriter + */ + +@Provider +@Consumes(MediaType.WILDCARD) +@Produces(MediaType.WILDCARD) +public class FastJson2Provider + implements MessageBodyReader, MessageBodyWriter { + /** + * These are classes that we never use for reading + * (never try to deserialize instances of these types). + */ + public static final Class[] DEFAULT_UNREADABLES = new Class[]{ + InputStream.class, Reader.class + }; + + /** + * These are classes that we never use for writing + * (never try to serialize instances of these types). + */ + public static final Class[] DEFAULT_UNWRITABLES = new Class[]{ + InputStream.class, + OutputStream.class, Writer.class, + StreamingOutput.class, Response.class + }; + + /** + * Injectable context object used to locate configured + * instance of {@link FastJsonConfig} to use for actual + * serialization. + */ + @Context + protected Providers providers; + + /** + * with fastJson config + */ + @Getter + @Setter + private FastJsonConfig fastJsonConfig = new FastJsonConfig(); + + /** + * allow serialize/deserialize types in clazzes + */ + private Class[] clazzes; + + /** + * Can serialize/deserialize all types. + */ + public FastJson2Provider() { + this((Class[]) null); + } + + /** + * Only serialize/deserialize all types in clazzes. + */ + public FastJson2Provider(Class[] clazzes) { + this.clazzes = clazzes; + } + + /** + * Check some are interface/abstract classes to exclude. + * + * @param type the type + * @param classes the classes + * @return the boolean + */ + protected boolean isNotAssignableFrom(Class type, Class[] classes) { + if (type == null) { + return true; + } + // there are some other abstract/interface types to exclude too: + for (Class cls : classes) { + if (cls.isAssignableFrom(type)) { + return true; + } + } + return false; + } + + /** + * Check whether a class can be serialized or deserialized. It can check + * based on packages, annotations on entities or explicit classes. + * + * @param type class need to check + * @return true if valid + */ + protected boolean isValidType(Class type, Annotation[] classAnnotations) { + if (type == null) { + return false; + } + if (clazzes != null) { + for (Class cls : clazzes) { + // must strictly equal. Don't check inheritance + if (cls == type) { + return true; + } + } + return false; + } + return true; + } + + /** + * Check media type like "application/json". + * + * @param mediaType media type + * @return true if the media type is valid + */ + protected boolean hasNotMatchingMediaType(MediaType mediaType) { + if (mediaType == null) { + return false; + } + String subtype = mediaType.getSubtype(); + return !(("json".equalsIgnoreCase(subtype)) + || (subtype.endsWith("+json")) + || ("javascript".equals(subtype)) + || ("x-javascript".equals(subtype)) + || ("x-json".equals(subtype)) + || ("x-www-form-urlencoded".equalsIgnoreCase(subtype)) + || (subtype.endsWith("x-www-form-urlencoded"))); + } + + /** + * Method that JAX-RS container calls to try to check whether given value + * (of specified type) can be serialized by this provider. + */ + @Override + public boolean isWriteable( + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType) { + if (hasNotMatchingMediaType(mediaType)) { + return false; + } + if (isNotAssignableFrom(type, DEFAULT_UNWRITABLES)) { + return false; + } + return isValidType(type, annotations); + } + + /** + * Method that JAX-RS container calls to serialize given value. + */ + @Override + public void writeTo( + Object object, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream + ) throws IOException, WebApplicationException { + FastJsonConfig config = locateConfigProvider(type, mediaType); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + if (object instanceof String && JSON.isValidObject((String) object)) { + byte[] strBytes = ((String) object).getBytes(config.getCharset()); + baos.write(strBytes, 0, strBytes.length); + } else if (object instanceof byte[] && JSON.isValid((byte[]) object)) { + byte[] strBytes = (byte[]) object; + baos.write(strBytes, 0, strBytes.length); + } else { + JSON.writeTo( + baos, + object, + config.getDateFormat(), + config.getWriterFilters(), + config.getWriterFeatures() + ); + } + baos.writeTo(entityStream); + } + } + + /** + * Method that JAX-RS container calls to try to check whether values of + * given type (and media type) can be deserialized by this provider. + */ + @Override + public boolean isReadable( + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType) { + if (hasNotMatchingMediaType(mediaType)) { + return false; + } + if (isNotAssignableFrom(type, DEFAULT_UNREADABLES)) { + return false; + } + return isValidType(type, annotations); + } + + /** + * Method that JAX-RS container calls to deserialize given value. + */ + @Override + public Object readFrom( + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + InputStream entityStream + ) throws IOException, WebApplicationException { + FastJsonConfig config = locateConfigProvider(type, mediaType); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buf = new byte[1024 * 64]; + for (; ; ) { + int len = entityStream.read(buf); + if (len == -1) { + break; + } + if (len > 0) { + baos.write(buf, 0, len); + } + } + byte[] bytes = baos.toByteArray(); + return JSON.parseObject(bytes, genericType, config.getDateFormat(), config.getReaderFilters(), config.getReaderFeatures()); + } catch (JSONException ex) { + throw new WebApplicationException(ex); + } + } + + /** + * Helper method that is called if no config has been explicitly configured. + */ + protected FastJsonConfig locateConfigProvider(Class type, MediaType mediaType) { + if (providers != null) { + ContextResolver resolver = providers.getContextResolver(FastJsonConfig.class, mediaType); + if (resolver == null) { + resolver = providers.getContextResolver(FastJsonConfig.class, null); + } + if (resolver != null) { + return resolver.getContext(type); + } + } + return fastJsonConfig; + } +} diff --git a/extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyReader b/extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyReader new file mode 100644 index 0000000000..f8dd360c83 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyReader @@ -0,0 +1 @@ +com.alibaba.fastjson2.support.jaxrs.javax.FastJson2Provider diff --git a/extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyWriter b/extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyWriter new file mode 100644 index 0000000000..f8dd360c83 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyWriter @@ -0,0 +1 @@ +com.alibaba.fastjson2.support.jaxrs.javax.FastJson2Provider diff --git a/extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers b/extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers new file mode 100644 index 0000000000..f8dd360c83 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers @@ -0,0 +1 @@ +com.alibaba.fastjson2.support.jaxrs.javax.FastJson2Provider diff --git a/extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable b/extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable new file mode 100644 index 0000000000..e7142f08cf --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-javax/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable @@ -0,0 +1 @@ +com.alibaba.fastjson2.support.jaxrs.javax.FastJson2AutoDiscoverable diff --git a/extension-jaxrs/extension-jaxrs-javax/src/test/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2ProviderTest.java b/extension-jaxrs/extension-jaxrs-javax/src/test/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2ProviderTest.java new file mode 100644 index 0000000000..73fd29a3e5 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-javax/src/test/java/com/alibaba/fastjson2/support/jaxrs/javax/FastJson2ProviderTest.java @@ -0,0 +1,83 @@ +package com.alibaba.fastjson2.support.jaxrs.javax; + +import com.alibaba.fastjson2.support.jaxrs.javax.model.User; +import lombok.SneakyThrows; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author 张治保 + * @since 2024/10/16 + */ +public class FastJson2ProviderTest + extends JerseyTest { + private static final FastJson2Provider fastJson2Provider = new FastJson2Provider(); + + @Override + protected Application configure() { + enable(TestProperties.LOG_TRAFFIC); + enable(TestProperties.DUMP_ENTITY); + return new ResourceConfig(JaxRsResource.class) + .register(fastJson2Provider, MessageBodyReader.class, MessageBodyWriter.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(fastJson2Provider, MessageBodyReader.class, MessageBodyWriter.class); + } + + @Test + @SneakyThrows + void testGet() { + String json = successAndGet( + target().path("/test/get") + .request() + .get() + ); + JSONAssert.assertEquals("{\"name\":\"fastjson2\",\"age\":0}", json, true); + } + + @SneakyThrows + @Test + void testGetPath() { + //MODULE_ORDER: One does not simply declare modules! + String json = successAndGet( + target().path("/test/get/hello-fastjson2") + .request() + .get() + ); + JSONAssert.assertEquals("{\"name\":\"hello-fastjson2\",\"age\":0}", json, true); + } + + @Test + @SneakyThrows + void testPost() { + User user = new User() + .setName("fastjson2") + .setAge(1); + String json = successAndGet( + target().path("/test/post") + .request(MediaType.APPLICATION_JSON) + .post(Entity.json(user)) + ); + JSONAssert.assertEquals("{\"name\":\"fastjson2\",\"age\":1}", json, true); + } + + public static String successAndGet(Response response) { + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + return response.readEntity(String.class); + } +} diff --git a/extension-jaxrs/extension-jaxrs-javax/src/test/java/com/alibaba/fastjson2/support/jaxrs/javax/JaxRsResource.java b/extension-jaxrs/extension-jaxrs-javax/src/test/java/com/alibaba/fastjson2/support/jaxrs/javax/JaxRsResource.java new file mode 100644 index 0000000000..1551d206c1 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-javax/src/test/java/com/alibaba/fastjson2/support/jaxrs/javax/JaxRsResource.java @@ -0,0 +1,33 @@ +package com.alibaba.fastjson2.support.jaxrs.javax; + +import com.alibaba.fastjson2.support.jaxrs.javax.model.User; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +/** + * @author 张治保 + * @since 2024/10/16 + */ +@Path("/test") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class JaxRsResource { + @GET + @Path("/get") + public User get() { + return new User().setName("fastjson2"); + } + + @GET + @Path("/get/{name}") + public User getPath(@PathParam("name") String name) { + return new User().setName(name); + } + + @POST + @Path("/post") + public User post(User user) { + return user.setAge(user.getAge()) + .setName(user.getName()); + } +} diff --git a/extension-jaxrs/extension-jaxrs-javax/src/test/java/com/alibaba/fastjson2/support/jaxrs/javax/model/User.java b/extension-jaxrs/extension-jaxrs-javax/src/test/java/com/alibaba/fastjson2/support/jaxrs/javax/model/User.java new file mode 100644 index 0000000000..f8ceeaf697 --- /dev/null +++ b/extension-jaxrs/extension-jaxrs-javax/src/test/java/com/alibaba/fastjson2/support/jaxrs/javax/model/User.java @@ -0,0 +1,19 @@ +package com.alibaba.fastjson2.support.jaxrs.javax.model; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * @author 张治保 + * @since 2024/10/16 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class User { + private String name; + private int age; +} diff --git a/extension-jaxrs/pom.xml b/extension-jaxrs/pom.xml new file mode 100644 index 0000000000..af65d70a5a --- /dev/null +++ b/extension-jaxrs/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + com.alibaba.fastjson2 + fastjson2-parent + 2.0.54-SNAPSHOT + + fastjson2-extension-jaxrs + pom + + + 2.1.1 + 3.1.0 + + + + + extension-jaxrs-javax + + + + + com.alibaba.fastjson2 + fastjson2-extension + ${project.version} + + + + org.projectlombok + lombok + provided + + + + + + diff --git a/pom.xml b/pom.xml index 56680fb36a..f61165b51b 100644 --- a/pom.xml +++ b/pom.xml @@ -78,6 +78,7 @@ example-spring6-test --> extension + extension-jaxrs extension-solon extension-spring5