diff --git a/README.md b/README.md index 23fd70c..5a43a35 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,105 @@ public class FuryResources { } ``` +## Use Apache Fury with Quarkus REST/RESTEasy +You can send a http request with Fury protocol, and let Fury to handle your objects serialization. + +The usage will be different if class registration is disabled or enabled: +- Enable class registration: you must register class with same ID as the server, you should assign an id using +`@FurySerialization(classId = xxx)`, otherwise Fury will allocate an auto-generated ID which you won't know at the +client for registration. +- Disable class registration: no class id are needed to register, which is more easy to use, but the serialized size +will be larger since Fury will serialize class as a string instead of an id. Note that `quarkus-fury` will only allow +classes annotated by `@FurySerialization` for deserialization, the deserialization will be safe as class registration +enabled. + +### Class registration enabled +Server example: +```java +import io.quarkiverse.fury.FurySerialization; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +@FurySerialization(classId = 100) +record Struct(int f1, String f2) { +} + +@Path("/fury") +@ApplicationScoped +public class FuryResources { + @POST + @Path("/struct") + @Produces("application/fury") + @Consumes("application/fury") + public Struct testStruct(Struct obj) { + return new Struct(10, "abc"); + } +} +``` + +Client example: +```java +import static io.restassured.RestAssured.given; +import org.apache.fury.ThreadSafeFury; +import io.restassured.RestAssured; +import io.restassured.response.Response; + +public class FuryClient { + private static ThreadSafeFury fury = Fury.builder().requireClassRegistration(false).buildThreadSafeFury(); + static { + fury.register(Struct.class, 100, true); + } + + public static void main(String[] args) { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + Struct struct = Struct.create(); + Response response = given().contentType("application/fury").body(fury.serialize(struct)).when() + .post("/fury/struct").then().statusCode(200).contentType("application/fury").extract().response(); + byte[] result = response.body().asByteArray(); + Struct struct1 = (Struct) fury.deserialize(result); + System.out.println(struct1); + } +} +``` + +### Class registration disabled +Server example: +```java +@FurySerialization +record Struct(int f1, String f2) { +} + +@Path("/fury") +@ApplicationScoped +public class FuryResources { + @POST + @Path("/struct") + @Produces("application/fury") + @Consumes("application/fury") + public Struct testStruct(Struct obj) { + return new Struct(10, "abc"); + } +} +``` + +Client example +```java +public class RestClient { + private static ThreadSafeFury fury = Fury.builder().requireClassRegistration(false).buildThreadSafeFury(); + + public static void main(String[] args) { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + Struct struct = Struct.create(); + Response response = given().contentType("application/fury").body(fury.serialize(struct)).when() + .post("/fury/struct").then().statusCode(200).contentType("application/fury").extract().response(); + byte[] result = response.body().asByteArray(); + Struct struct1 = (Struct) fury.deserialize(result); + System.out.println(struct1); + } +} +``` + More details about usage can be found [here](https://docs.quarkiverse.io/quarkus-fury/dev/index.html). ## Contributors ✨ diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index ec6976a..dc0c891 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -116,7 +116,8 @@ public class ExampleResource { } ---- -NOTE: The class ID (or serialization identifier) must be identical on both the client and server sides. A mismatch in the class ID will result in serialization or deserialization issues, potentially causing runtime errors or data corruption. +NOTE: If class registration is enabled, the class ID (or serialization identifier) must be identical on both the client and server sides. A mismatch in the class ID will result in serialization or deserialization issues, potentially causing runtime errors or data corruption. +If class registration is disabled, please do not assign class id at client and server slides. [[extension-configuration-reference]] == Extension Configuration Reference diff --git a/integration-tests/commons/src/main/java/io/quarkiverse/fury/it/FuryResources.java b/integration-tests/commons/src/main/java/io/quarkiverse/fury/it/FuryResources.java index c2f5fc1..89ed2c8 100644 --- a/integration-tests/commons/src/main/java/io/quarkiverse/fury/it/FuryResources.java +++ b/integration-tests/commons/src/main/java/io/quarkiverse/fury/it/FuryResources.java @@ -82,6 +82,15 @@ public Boolean testSerializePOJO() { return struct1.equals(struct2); } + @POST + @Path("/struct") + @Produces("application/fury") + @Consumes("application/fury") + public Struct testStruct(Struct obj) { + Preconditions.checkArgument(obj.equals(Struct.create()), obj); + return Struct.create(); + } + @POST @Path("/test") @Produces("application/fury") diff --git a/integration-tests/commons/src/main/resources/application.properties b/integration-tests/commons/src/main/resources/application.properties index 229c348..8d281df 100644 --- a/integration-tests/commons/src/main/resources/application.properties +++ b/integration-tests/commons/src/main/resources/application.properties @@ -1,5 +1,5 @@ quarkus.rest-client."io.quarkiverse.fury.it.Client".url=${test.url} - +quarkus.fury.required-class-registration=false quarkus.fury.register-class-names=io.quarkiverse.fury.it.Struct quarkus.fury.register-class."io.quarkiverse.fury.it.Foo".class-id=300 quarkus.fury.register-class."io.quarkiverse.fury.it.Foo".serializer=io.quarkiverse.fury.it.FooSerializer diff --git a/integration-tests/commons/src/test/java/io/quarkiverse/fury/it/FuryTest.java b/integration-tests/commons/src/test/java/io/quarkiverse/fury/it/FuryTest.java index f03290e..7da0606 100644 --- a/integration-tests/commons/src/test/java/io/quarkiverse/fury/it/FuryTest.java +++ b/integration-tests/commons/src/test/java/io/quarkiverse/fury/it/FuryTest.java @@ -3,7 +3,6 @@ import static io.quarkiverse.fury.it.FuryResources.BAR_CLASS_ID; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; import org.apache.fury.Fury; import org.junit.jupiter.api.Assertions; @@ -29,6 +28,18 @@ public void testThirdPartyBar() { given().when().get("/fury/third_party_bar").then().statusCode(200).body(is("true")); } + @Test + public void testFuryStruct() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + Struct struct = Struct.create(); + Fury fury = Fury.builder().requireClassRegistration(false).withName("Fury" + System.nanoTime()).build(); + Response response = given().contentType("application/fury").body(fury.serialize(struct)).when() + .post("/fury/struct").then().statusCode(200).contentType("application/fury").extract().response(); + byte[] result = response.body().asByteArray(); + Struct struct1 = (Struct) fury.deserialize(result); + Assertions.assertEquals(struct1, struct); + } + @Test public void testFuryBar() { RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); diff --git a/runtime/src/main/java/io/quarkiverse/fury/FuryRecorder.java b/runtime/src/main/java/io/quarkiverse/fury/FuryRecorder.java index 7bc328d..2fc8eb8 100644 --- a/runtime/src/main/java/io/quarkiverse/fury/FuryRecorder.java +++ b/runtime/src/main/java/io/quarkiverse/fury/FuryRecorder.java @@ -1,13 +1,21 @@ package io.quarkiverse.fury; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Optional; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.function.Function; import org.apache.fury.BaseFury; import org.apache.fury.Fury; +import org.apache.fury.ThreadLocalFury; import org.apache.fury.ThreadSafeFury; +import org.apache.fury.config.Config; import org.apache.fury.config.FuryBuilder; +import org.apache.fury.resolver.ClassChecker; import org.apache.fury.resolver.ClassResolver; import org.apache.fury.serializer.Serializer; +import org.apache.fury.util.GraalvmSupport; import org.apache.fury.util.Preconditions; import org.jboss.logging.Logger; @@ -18,6 +26,9 @@ @Recorder public class FuryRecorder { private static final Logger LOG = Logger.getLogger(FuryRecorder.class); + private static final ConcurrentSkipListSet annotatedClasses = new ConcurrentSkipListSet<>(); + private static final ClassChecker checker = (classResolver, className) -> !GraalvmSupport.isGraalRuntime() + || annotatedClasses.contains(className); public RuntimeValue createFury( final FuryBuildTimeConfig config, final BeanContainer beanContainer) { @@ -32,8 +43,13 @@ public RuntimeValue createFury( .deserializeNonexistentEnumValueAsNull(config.deserializeNonexistentEnumValueAsNull()) .withNumberCompressed(config.compressNumber()) .withStringCompressed(config.compressString()); - BaseFury fury = config.threadSafe() ? builder.buildThreadSafeFury() : builder.build(); - + Function furyFactory = c -> { + Fury f = builder.withClassLoader(c).build(); + f.getClassResolver().setClassChecker(checker); + return f; + }; + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + BaseFury fury = config.threadSafe() ? new ThreadLocalFury(furyFactory) : furyFactory.apply(classLoader); // register to the container beanContainer.beanInstance(FuryProducer.class).setFury(fury); return new RuntimeValue<>(fury); @@ -49,6 +65,7 @@ public void registerClassByName(final RuntimeValue furyValue, final St .loadClass(serializerClassName.get()); } registerClass(furyValue, clazz, classId, serializer); + annotatedClasses.add(className); } catch (ClassNotFoundException e) { LOG.warn("can not find register class: " + className + (serializerClassName.isPresent() @@ -61,29 +78,17 @@ public void registerClassByName(final RuntimeValue furyValue, final St public void registerClass( final RuntimeValue furyValue, final Class clazz, final int classId, Class serializer) { + annotatedClasses.add(clazz.getName()); BaseFury fury = furyValue.getValue(); + ClassResolver classResolver = getClassResolver(fury); + Config config = classResolver.getFury().getConfig(); if (classId > 0) { Preconditions.checkArgument( classId >= 256 && classId <= Short.MAX_VALUE, "Class id %s must be >= 256 and <= %s", classId, Short.MAX_VALUE); - Class registeredClass; - if (fury instanceof ThreadSafeFury) { - ThreadSafeFury threadSafeFury = (ThreadSafeFury) fury; - registeredClass = (threadSafeFury).execute(f -> f.getClassResolver().getRegisteredClass((short) classId)); - if (serializer == null) { - // Generate serializer bytecode. - threadSafeFury.execute(f -> f.getClassResolver().getSerializerClass(clazz)); - } - } else { - ClassResolver classResolver = ((Fury) fury).getClassResolver(); - registeredClass = classResolver.getRegisteredClass((short) classId); - if (serializer == null) { - // Generate serializer bytecode. - classResolver.getSerializerClass(clazz); - } - } + Class registeredClass = classResolver.getRegisteredClass((short) classId); Preconditions.checkArgument( registeredClass == null, "ClassId %s has been registered for class %s", @@ -92,10 +97,31 @@ public void registerClass( fury.register(clazz, (short) classId); } else { // Generate serializer bytecode. - fury.register(clazz, serializer == null); + if (config.requireClassRegistration()) { + fury.register(clazz, serializer == null); + } } if (serializer != null) { fury.registerSerializer(clazz, serializer); + } else { + // Generate serializer bytecode. + try { + Method createSerializerAhead = ClassResolver.class.getDeclaredMethod( + "createSerializerAhead", Class.class); + createSerializerAhead.setAccessible(true); + createSerializerAhead.invoke(classResolver, clazz); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } + + private ClassResolver getClassResolver(BaseFury fury) { + if (fury instanceof ThreadSafeFury) { + ThreadSafeFury threadSafeFury = (ThreadSafeFury) fury; + return threadSafeFury.execute(Fury::getClassResolver); + } else { + return ((Fury) fury).getClassResolver(); } } } diff --git a/runtime/src/main/java/io/quarkiverse/fury/FurySerializer.java b/runtime/src/main/java/io/quarkiverse/fury/FurySerializer.java index 29c3cfb..58d33d9 100644 --- a/runtime/src/main/java/io/quarkiverse/fury/FurySerializer.java +++ b/runtime/src/main/java/io/quarkiverse/fury/FurySerializer.java @@ -20,6 +20,8 @@ import org.apache.fury.ThreadSafeFury; import org.apache.fury.io.FuryInputStream; import org.apache.fury.resolver.ClassResolver; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; @@ -64,6 +66,11 @@ protected boolean isSupportedMediaType(MediaType mediaType) { } protected boolean canSerialize(final Class aClass) { + Config config = ConfigProvider.getConfig(); + Boolean requiredClassRegistration = config.getValue("quarkus.fury.required-class-registration", Boolean.class); + if (!requiredClassRegistration) { + return true; + } if (getFury() instanceof final ThreadSafeFury threadSafeFury) { return (threadSafeFury).execute(f -> f.getClassResolver().getRegisteredClassId(aClass)) != null; } else {